Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 10 Sep 2014 18:36:26 -0400
changeset 204702 bc7deafdac4b8553ec45e3111e3fa38ef4de3eb0
parent 204701 280ac54e2dd89dfcb80fc28eaf66f42148f91476 (current diff)
parent 204608 bad6a9fc2bf07d1ad360b037233bb6d84a2b3e8e (diff)
child 204741 0ef5e1a0486ab9a478fb8a3edea62fdacf4a8a56
child 204743 ed2fb19942d0846fe15acbf69b99dda4fa46d453
child 204810 7962b4aaceb5a9413582cf3f0389863bc3394da5
child 204828 2a6be99b3452cfebd44759dd5f36e28cbdbcefb9
push id27463
push userryanvm@gmail.com
push dateThu, 11 Sep 2014 00:30:54 +0000
treeherdermozilla-central@bc7deafdac4b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone35.0a1
first release with
nightly linux32
bc7deafdac4b / 35.0a1 / 20140911030204 / files
nightly linux64
bc7deafdac4b / 35.0a1 / 20140911030204 / files
nightly mac
bc7deafdac4b / 35.0a1 / 20140911030204 / files
nightly win32
bc7deafdac4b / 35.0a1 / 20140911030204 / files
nightly win64
bc7deafdac4b / 35.0a1 / 20140911030204 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound. a=merge CLOSED TREE
browser/app/profile/firefox.js
browser/devtools/shared/test/browser_graphs-07.js
dom/bindings/BindingUtils.h
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
@@ -33,17 +33,17 @@
   <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"/>
   <project name="device/sample" path="device/sample" revision="1a3d8efa0ad32ec8f145367a3cf0f54b97385c3c"/>
   <project name="platform/abi/cpp" path="abi/cpp" revision="18f1b5e28734183ff8073fe86dc46bc4ebba8a59"/>
   <project name="platform/bionic" path="bionic" revision="86b1f589c313422a7da1812512b9ec8d1cf9ba3c"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="4eece0d80928a2b5266b78421ebf0c8686d4ad2c"/>
   <project name="platform/external/aac" path="external/aac" revision="fa3eba16446cc8f2f5e2dfc20d86a49dbd37299e"/>
   <project name="platform/external/bison" path="external/bison" revision="c2418b886165add7f5a31fc5609f0ce2d004a90e"/>
-  <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="c50830cae1b748024eec7e73ad98a4e427f663c7"/>
+  <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="c8e99ca7e11c00f8124196fe1726a15e6e976587"/>
   <project name="platform/external/bsdiff" path="external/bsdiff" revision="23e322ab19fb7d74c2c37e40ce364d9f709bdcee"/>
   <project name="platform/external/bzip2" path="external/bzip2" revision="1cb636bd8e9e5cdfd5d5b2909a122f6e80db62de"/>
   <project name="platform/external/checkpolicy" path="external/checkpolicy" revision="0d73ef7049feee794f14cf1af88d05dae8139914"/>
   <project name="platform/external/dhcpcd" path="external/dhcpcd" revision="84b7252b0a9d0edc9a1db1e0c518771d26b23058"/>
   <project name="platform/external/dnsmasq" path="external/dnsmasq" revision="41d356427a632f5336384bfa45c8420ffc274f66"/>
   <project name="platform/external/dropbear" path="external/dropbear" revision="a34ddbe3819bc465968f3676c734b405e655f8b7"/>
   <project name="platform/external/e2fsprogs" path="external/e2fsprogs" revision="47478a2944a2a17c7fdebe9d92573db92013125c"/>
   <project name="platform/external/elfutils" path="external/elfutils" revision="b23b2dfb354b3ccf5d1c5d39815f02e7048cf516"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -10,21 +10,21 @@
   <!--original fetch url was git://github.com/mozilla/-->
   <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!--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="700b031a54079f791344aa091798f6b43a9e2900">
+  <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <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"/>
@@ -125,16 +125,16 @@
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
-  <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="c7ccf6eff27f99e39a9eca94cde48aaece5e47db"/>
+  <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="125ccf9bd5986c7728ea44508b3e1d1185ac028b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d259117b4976decbe2f76eeed85218bf0109190f"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/>
   <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="54a712b46fe937dacdaed9b1261c63847129a719"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/>
   <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/>
 </manifest>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -10,21 +10,21 @@
   <!--original fetch url was git://github.com/mozilla/-->
   <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!--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="700b031a54079f791344aa091798f6b43a9e2900">
+  <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
@@ -127,17 +127,17 @@
   <remove-project name="platform/hardware/libhardware"/>
   <remove-project name="platform/external/bluetooth/bluedroid"/>
   <!--original fetch url was git://github.com/t2m-foxfone/-->
   <remote fetch="https://git.mozilla.org/external/t2m-foxfone" name="t2m"/>
   <default remote="caf" revision="LNX.LA.3.5.2.1.1" sync-j="4"/>
   <!-- Flame specific things -->
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="1bb28abbc215f45220620af5cd60a8ac1be93722"/>
   <project name="device/qcom/common" path="device/qcom/common" revision="54c32c2ddef066fbdf611d29e4b7c47e0363599e"/>
-  <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="540314ae9c56394c6b1f17a267db9f25c5acb9d6"/>
+  <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="8f988f3950da8d55676b3b77b09d5722b967e07b"/>
   <project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="893238eb1215f8fd4f3747169170cc5e1cc33969"/>
   <project name="kernel_lk" path="bootable/bootloader/lk" remote="b2g" revision="9e62af4da848d56841bdde326f9bba26c743c33a"/>
   <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="082a1f98422e6a6b56f61218d6fcf465e85d4c58"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="5b71e40213f650459e95d35b6f14af7e88d8ab62"/>
   <project name="platform_external_libnfc-nci" path="external/libnfc-nci" remote="t2m" revision="4186bdecb4dae911b39a8202252cc2310d91b0be"/>
   <project name="platform/frameworks/av" path="frameworks/av" revision="c1814713bd2d07c2af0c236007badc8732a34324"/>
   <project name="platform/frameworks/base" path="frameworks/base" revision="6b58ab45e3e56c1fc20708cc39fa2264c52558df"/>
   <project name="platform/frameworks/native" path="frameworks/native" revision="a46a9f1ac0ed5662d614c277cbb14eb3f332f365"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "a4a76a4221d7d963d01377f38d68768d0e829017", 
+    "revision": "6465db9982731ec95ad344901af20086ad94291f", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -8,21 +8,21 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
-  <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900">
+  <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -6,21 +6,21 @@
   <!--original fetch url was git://github.com/mozilla/-->
   <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/>
   <!--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"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
-  <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900">
+  <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -8,21 +8,21 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
-  <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900">
+  <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1365,18 +1365,19 @@ pref("devtools.debugger.tracer", false);
 // The default Debugger UI settings
 pref("devtools.debugger.ui.panes-sources-width", 200);
 pref("devtools.debugger.ui.panes-instruments-width", 300);
 pref("devtools.debugger.ui.panes-visible-on-startup", false);
 pref("devtools.debugger.ui.variables-sorting-enabled", true);
 pref("devtools.debugger.ui.variables-only-enum-visible", false);
 pref("devtools.debugger.ui.variables-searchbox-visible", false);
 
-// Enable the Profiler
+// Enable the Profiler and the Timeline
 pref("devtools.profiler.enabled", true);
+pref("devtools.timeline.enabled", false);
 
 // The default Profiler UI settings
 pref("devtools.profiler.ui.show-platform-data", false);
 
 // The default cache UI setting
 pref("devtools.cache.disabled", false);
 
 // Enable the Network Monitor
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -1,12 +1,21 @@
 # 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/.
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+#ifdef MOZ_SERVICES_CLOUDSYNC
+XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+                                  "resource://gre/modules/CloudSync.jsm");
+#else
+let CloudSync = null;
+#endif
+
 // gSyncUI handles updating the tools menu and displaying notifications.
 let gSyncUI = {
   DEFAULT_EOL_URL: "https://www.mozilla.org/firefox/?utm_source=synceol",
 
   _obs: ["weave:service:sync:start",
          "weave:service:quota:remaining",
          "weave:service:setup-complete",
          "weave:service:login:start",
@@ -117,17 +126,19 @@ let gSyncUI = {
     let needsSetup = this._needsSetup();
     let loginFailed = this._loginFailed();
 
     // Start off with a clean slate
     document.getElementById("sync-reauth-state").hidden = true;
     document.getElementById("sync-setup-state").hidden = true;
     document.getElementById("sync-syncnow-state").hidden = true;
 
-    if (loginFailed) {
+    if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
+      document.getElementById("sync-syncnow-state").hidden = false;
+    } else if (loginFailed) {
       document.getElementById("sync-reauth-state").hidden = false;
     } else if (needsSetup) {
       document.getElementById("sync-setup-state").hidden = false;
     } else {
       document.getElementById("sync-syncnow-state").hidden = false;
     }
 
     if (!gBrowser)
@@ -270,17 +281,24 @@ let gSyncUI = {
 
   openServerStatus: function () {
     let statusURL = Services.prefs.getCharPref("services.sync.statusURL");
     window.openUILinkIn(statusURL, "tab");
   },
 
   // Commands
   doSync: function SUI_doSync() {
-    setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0);
+    let needsSetup = this._needsSetup();
+    let loginFailed = this._loginFailed();
+
+    if (!(loginFailed || needsSetup)) {
+      setTimeout(function () Weave.Service.errorHandler.syncAndReportErrors(), 0);
+    }
+
+    Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
   },
 
   handleToolbarButton: function SUI_handleStatusbarButton() {
     if (this._needsSetup())
       this.openSetup();
     else
       this.doSync();
   },
--- a/browser/base/content/test/general/browser_bug575561.js
+++ b/browser/base/content/test/general/browser_bug575561.js
@@ -1,84 +1,84 @@
-function test() {
-  waitForExplicitFinish();
-
-  // Pinned: Link to the same domain should not open a new tab
-  // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html  
-  testLink(0, true, false, function() {
-    // Pinned: Link to a different subdomain should open a new tab
-    // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html
-    testLink(1, true, true, function() {
-      // Pinned: Link to a different domain should open a new tab
-      // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
-      testLink(2, true, true, function() {
-        // Not Pinned: Link to a different domain should not open a new tab
-        // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
-        testLink(2, false, false, function() {
-          // Pinned: Targetted link should open a new tab
-          // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo"
-          testLink(3, true, true, function() {
-            // Pinned: Link in a subframe should not open a new tab
-            // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe
-            testLink(0, true, false, function() {
-              // Pinned: Link to the same domain (with www prefix) should not open a new tab
-              // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html      
-              testLink(4, true, false, function() {
-                // Pinned: Link to a data: URI should not open a new tab
-                // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
-                testLink(5, true, false, function() {
-                  // Pinned: Link to an about: URI should not open a new tab
-                  // Tests link to about:mozilla
-                  testLink(6, true, false, finish);
-                });
-              });
-            }, true);
-          });
-        });
-      });
-    });
-  });
-}
-
-function testLink(aLinkIndex, pinTab, expectNewTab, nextTest, testSubFrame) {
-  let appTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/general/app_bug575561.html", {skipAnimation: true});
-  if (pinTab)
-    gBrowser.pinTab(appTab);
-  gBrowser.selectedTab = appTab;
-
-  waitForDocLoadComplete(appTab.linkedBrowser).then(function() {
-    let browser = gBrowser.getBrowserForTab(appTab);
-    if (testSubFrame)
-      browser = browser.contentDocument.getElementsByTagName("iframe")[0];
-
-    let links = browser.contentDocument.getElementsByTagName("a");
-
-    if (expectNewTab)
-      gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
-    else
-      waitForDocLoadComplete(appTab.linkedBrowser).then(onPageLoad);
-
-    info("Clicking " + links[aLinkIndex].textContent);
-    EventUtils.sendMouseEvent({type:"click"}, links[aLinkIndex], browser.contentWindow);
-    let linkLocation = links[aLinkIndex].href;
-
-    function onPageLoad() {
-      browser.removeEventListener("load", onPageLoad, true);
-      is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab");
-      executeSoon(function(){
-        gBrowser.removeTab(appTab);
-        nextTest();
-      });
-    }
-
-    function onTabOpen(event) {
-      gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
-      ok(true, "Link should open a new tab");
-      waitForDocLoadComplete(event.target.linkedBrowser).then(function() {
-        executeSoon(function(){
-          gBrowser.removeTab(appTab);
-          gBrowser.removeCurrentTab();
-          nextTest();
-        });
-      });
-    }
-  });
-}
+const TEST_URL = "http://example.com/browser/browser/base/content/test/general/app_bug575561.html";
+
+add_task(function*() {
+  SimpleTest.requestCompleteLog();
+
+  // Pinned: Link to the same domain should not open a new tab
+  // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html
+  yield testLink(0, true, false);
+  // Pinned: Link to a different subdomain should open a new tab
+  // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html
+  yield testLink(1, true, true);
+
+  // Pinned: Link to a different domain should open a new tab
+  // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
+  yield testLink(2, true, true);
+
+  // Not Pinned: Link to a different domain should not open a new tab
+  // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
+  yield testLink(2, false, false);
+
+  // Pinned: Targetted link should open a new tab
+  // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo"
+  yield testLink(3, true, true);
+
+  // Pinned: Link in a subframe should not open a new tab
+  // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe
+  yield testLink(0, true, false, true);
+
+  // Pinned: Link to the same domain (with www prefix) should not open a new tab
+  // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html
+  yield testLink(4, true, false);
+
+  // Pinned: Link to a data: URI should not open a new tab
+  // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
+  yield testLink(5, true, false);
+
+  // Pinned: Link to an about: URI should not open a new tab
+  // Tests link to about:mozilla
+  yield testLink(6, true, false);
+});
+
+let waitForPageLoad = Task.async(function*(browser, linkLocation) {
+  yield waitForDocLoadComplete();
+
+  is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab");
+});
+
+let waitForTabOpen = Task.async(function*() {
+  let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
+  ok(true, "Link should open a new tab");
+
+  yield waitForDocLoadComplete(event.target.linkedBrowser);
+  yield Promise.resolve();
+
+  gBrowser.removeCurrentTab();
+});
+
+let testLink = Task.async(function*(aLinkIndex, pinTab, expectNewTab, testSubFrame) {
+  let appTab = gBrowser.addTab(TEST_URL, {skipAnimation: true});
+  if (pinTab)
+    gBrowser.pinTab(appTab);
+  gBrowser.selectedTab = appTab;
+
+  yield waitForDocLoadComplete();
+
+  let browser = appTab.linkedBrowser;
+  if (testSubFrame)
+    browser = browser.contentDocument.querySelector("iframe");
+
+  let link = browser.contentDocument.querySelectorAll("a")[aLinkIndex];
+
+  let promise;
+  if (expectNewTab)
+    promise = waitForTabOpen();
+  else
+    promise = waitForPageLoad(browser, link.href);
+
+  info("Clicking " + link.textContent);
+  link.click();
+
+  yield promise;
+
+  gBrowser.removeTab(appTab);
+});
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -103,16 +103,29 @@ function waitForCondition(condition, nex
 }
 
 function promiseWaitForCondition(aConditionFn) {
   let deferred = Promise.defer();
   waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
   return deferred.promise;
 }
 
+function promiseWaitForEvent(object, eventName, capturing = false) {
+  return new Promise((resolve) => {
+    function listener(event) {
+      info("Saw " + eventName);
+      object.removeEventListener(eventName, listener, capturing);
+      resolve(event);
+    }
+
+    info("Waiting for " + eventName);
+    object.addEventListener(eventName, listener, capturing);
+  });
+}
+
 function getTestPlugin(aName) {
   var pluginName = aName || "Test Plug-in";
   var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
   var tags = ph.getPluginTags();
 
   // Find the test plugin
   for (var i = 0; i < tags.length; i++) {
     if (tags[i].name == pluginName)
@@ -438,16 +451,17 @@ function waitForDocLoadAndStopIt(aExpect
  * @return promise
  */
 function waitForDocLoadComplete(aBrowser=gBrowser) {
   let deferred = Promise.defer();
   let progressListener = {
     onStateChange: function (webProgress, req, flags, status) {
       let docStart = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
                      Ci.nsIWebProgressListener.STATE_STOP;
+      info("Saw state " + flags.toString(16));
       if ((flags & docStart) == docStart) {
         aBrowser.removeProgressListener(progressListener);
         info("Browser loaded");
         deferred.resolve();
       }
     },
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                            Ci.nsISupportsWeakReference])
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -117,16 +117,33 @@ function injectLoopAPI(targetWindow) {
     locale: {
       enumerable: true,
       get: function() {
         return MozLoopService.locale;
       }
     },
 
     /**
+     * Returns the callData for a specific callDataId
+     *
+     * The data was retrieved from the LoopServer via a GET/calls/<version> request
+     * triggered by an incoming message from the LoopPushServer.
+     *
+     * @param {int} loopCallId
+     * @returns {callData} The callData or undefined if error.
+     */
+    getCallData: {
+      enumerable: true,
+      writable: true,
+      value: function(loopCallId) {
+        return Cu.cloneInto(MozLoopService.getCallData(loopCallId), targetWindow);
+      }
+    },
+
+    /**
      * Returns the contacts API.
      *
      * @returns {Object} The contacts API object
      */
     contacts: {
       enumerable: true,
       get: function() {
         if (contactsAPI) {
@@ -333,25 +350,34 @@ function injectLoopAPI(targetWindow) {
      * @param {Object} payloadObj An object which is converted to JSON and
      *                            transmitted with the request.
      * @param {Function} callback Called when the request completes.
      */
     hawkRequest: {
       enumerable: true,
       writable: true,
       value: function(path, method, payloadObj, callback) {
+        // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST
         // XXX Should really return a DOM promise here.
-        return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => {
+        return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => {
           callback(null, response.body);
         }, (error) => {
           callback(Cu.cloneInto(error, targetWindow));
         });
       }
     },
 
+    LOOP_SESSION_TYPE: {
+      enumerable: true,
+      writable: false,
+      value: function() {
+        return LOOP_SESSION_TYPE;
+      },
+    },
+
     logInToFxA: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.logInToFxA();
       }
     },
 
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -10,24 +10,29 @@ const { classes: Cc, interfaces: Ci, uti
 // https://github.com/mozilla-services/loop-server/blob/45787d34108e2f0d87d74d4ddf4ff0dbab23501c/loop/errno.json#L6
 const INVALID_AUTH_TOKEN = 110;
 
 // Ticket numbers are 24 bits in length.
 // The highest valid ticket number is 16777214 (2^24 - 2), so that a "now
 // serving" number of 2^24 - 1 is greater than it.
 const MAX_SOFT_START_TICKET_NUMBER = 16777214;
 
+const LOOP_SESSION_TYPE = {
+  GUEST: 1,
+  FXA: 2,
+};
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 
-this.EXPORTED_SYMBOLS = ["MozLoopService"];
+this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/devtools/Console.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
   "resource:///modules/loop/MozLoopAPI.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
@@ -76,16 +81,18 @@ let gErrors = new Map();
 /**
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
 let MozLoopServiceInternal = {
+  callsData: {data: undefined},
+
   // The uri of the Loop server.
   get loopServerUri() Services.prefs.getCharPref("loop.server"),
 
   /**
    * The initial delay for push registration. This ensures we don't start
    * kicking off straight after browser startup, just a few seconds later.
    */
   get initialRegistrationDelayMilliseconds() {
@@ -197,34 +204,36 @@ let MozLoopServiceInternal = {
       this.onHandleNotification.bind(this));
 
     return result;
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
+   *                                        This is one of the LOOP_SESSION_TYPE members.
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
    * @param {Object} payloadObj An object which is converted to JSON and
    *                            transmitted with the request.
    * @returns {Promise}
    *        Returns a promise that resolves to the response of the API call,
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
-  hawkRequest: function(path, method, payloadObj) {
+  hawkRequest: function(sessionType, path, method, payloadObj) {
     if (!gHawkClient) {
       gHawkClient = new HawkClient(this.loopServerUri);
     }
 
     let sessionToken;
     try {
-      sessionToken = Services.prefs.getCharPref("loop.hawk-session-token");
+      sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType));
     } catch (x) {
       // It is ok for this not to exist, we'll default to sending no-creds
     }
 
     let credentials;
     if (sessionToken) {
       // true = use a hex key, as required by the server (see bug 1032738).
       credentials = deriveHawkCredentials(sessionToken, "sessionToken",
@@ -232,29 +241,47 @@ let MozLoopServiceInternal = {
     }
 
     return gHawkClient.request(path, method, credentials, payloadObj).catch(error => {
       console.error("Loop hawkRequest error:", error);
       throw error;
     });
   },
 
+  getSessionTokenPrefName: function(sessionType) {
+    let suffix;
+    switch (sessionType) {
+      case LOOP_SESSION_TYPE.GUEST:
+        suffix = "";
+        break;
+      case LOOP_SESSION_TYPE.FXA:
+        suffix = ".fxa";
+        break;
+      default:
+        throw new Error("Unknown LOOP_SESSION_TYPE");
+        break;
+    }
+    return "loop.hawk-session-token" + suffix;
+  },
+
   /**
    * Used to store a session token from a request if it exists in the headers.
    *
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
+   *                                        One of the LOOP_SESSION_TYPE members.
    * @param {Object} headers The request headers, which may include a
    *                         "hawk-session-token" to be saved.
    * @return true on success or no token, false on failure.
    */
-  storeSessionToken: function(headers) {
+  storeSessionToken: function(sessionType, headers) {
     let sessionToken = headers["hawk-session-token"];
     if (sessionToken) {
       // XXX should do more validation here
       if (sessionToken.length === 64) {
-        Services.prefs.setCharPref("loop.hawk-session-token", sessionToken);
+        Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken);
       } else {
         // XXX Bubble the precise details up to the UI somehow (bug 1013248).
         console.warn("Loop server sent an invalid session token");
         gRegisteredDeferred.reject("session-token-wrong-size");
         gRegisteredDeferred = null;
         return false;
       }
     }
@@ -269,59 +296,70 @@ let MozLoopServiceInternal = {
    */
   onPushRegistered: function(err, pushUrl) {
     if (err) {
       gRegisteredDeferred.reject(err);
       gRegisteredDeferred = null;
       return;
     }
 
-    this.registerWithLoopServer(pushUrl);
+    this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => {
+      // storeSessionToken could have rejected and nulled the promise if the token was malformed.
+      if (!gRegisteredDeferred) {
+        return;
+      }
+      gRegisteredDeferred.resolve();
+      // No need to clear the promise here, everything was good, so we don't need
+      // to re-register.
+    }, (error) => {
+      Cu.reportError("Failed to register with Loop server: " + error.errno);
+      gRegisteredDeferred.reject(error.errno);
+      gRegisteredDeferred = null;
+    });
   },
 
   /**
-   * Registers with the Loop server.
+   * Registers with the Loop server either as a guest or a FxA user.
    *
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
    * @param {String} pushUrl The push url given by the push server.
-   * @param {Boolean} noRetry Optional, don't retry if authentication fails.
+   * @param {Boolean} [retry=true] Whether to retry if authentication fails.
+   * @return {Promise}
    */
-  registerWithLoopServer: function(pushUrl, noRetry) {
-    this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl})
+  registerWithLoopServer: function(sessionType, pushUrl, retry = true) {
+    return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURL: pushUrl})
       .then((response) => {
         // If this failed we got an invalid token. storeSessionToken rejects
         // the gRegisteredDeferred promise for us, so here we just need to
         // early return.
-        if (!this.storeSessionToken(response.headers))
+        if (!this.storeSessionToken(sessionType, response.headers))
           return;
 
         this.clearError("registration");
-        gRegisteredDeferred.resolve();
-        // No need to clear the promise here, everything was good, so we don't need
-        // to re-register.
       }, (error) => {
         // There's other errors than invalid auth token, but we should only do the reset
         // as a last resort.
         if (error.code === 401 && error.errno === INVALID_AUTH_TOKEN) {
           if (this.urlExpiryTimeIsInFuture()) {
             // XXX Should this be reported to the user is a visible manner?
             Cu.reportError("Loop session token is invalid, all previously "
                            + "generated urls will no longer work.");
           }
 
           // Authorization failed, invalid token, we need to try again with a new token.
-          Services.prefs.clearUserPref("loop.hawk-session-token");
-          this.registerWithLoopServer(pushUrl, true);
-          return;
+          Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType));
+          if (retry) {
+            return this.registerWithLoopServer(sessionType, pushUrl, false);
+          }
         }
 
         // XXX Bubble the precise details up to the UI somehow (bug 1013248).
         Cu.reportError("Failed to register with the loop server. error: " + error);
         this.setError("registration", error);
-        gRegisteredDeferred.reject(error.errno);
-        gRegisteredDeferred = null;
+        throw error;
       }
     );
   },
 
   /**
    * Callback from MozLoopPushHandler - A push notification has been received from
    * the server.
    *
@@ -332,19 +370,33 @@ let MozLoopServiceInternal = {
       return;
     }
 
     // We set this here as it is assumed that once the user receives an incoming
     // call, they'll have had enough time to see the terms of service. See
     // bug 1046039 for background.
     Services.prefs.setCharPref("loop.seenToS", "seen");
 
-    this.openChatWindow(null,
-                        this.localizedStrings["incoming_call_title2"].textContent,
-                        "about:loopconversation#incoming/" + version);
+    /* Request the information on the new call(s) associated with this version. */
+    this.hawkRequest(LOOP_SESSION_TYPE.GUEST,
+      "/calls?version=" + version, "GET").then(response => {
+      try {
+        let respData = JSON.parse(response.body);
+        if (respData.calls && respData.calls[0]) {
+          this.callsData.data = respData.calls[0];
+          this.openChatWindow(null,
+            this.localizedStrings["incoming_call_title2"].textContent,
+            "about:loopconversation#incoming/" + version);
+        } else {
+          console.warn("Error: missing calls[] in response");
+        }
+      } catch (err) {
+        console.warn("Error parsing calls info", err);
+      }
+    });
   },
 
   /**
    * A getter to obtain and store the strings for loop. This is structured
    * for use by l10n.js.
    *
    * @returns {Object} a map of element ids with attributes to set.
    */
@@ -504,17 +556,17 @@ let MozLoopServiceInternal = {
   },
 
   /**
    * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server.
    *
    * @return {Promise} resolved with the body of the hawk request for OAuth parameters.
    */
   promiseFxAOAuthParameters: function() {
-    return this.hawkRequest("/fxa-oauth/params", "POST").then(response => {
+    return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/params", "POST").then(response => {
       return JSON.parse(response.body);
     });
   },
 
   /**
    * Get the OAuth client constructed with Loop OAauth parameters.
    *
    * @return {Promise}
@@ -582,17 +634,17 @@ let MozLoopServiceInternal = {
     if (!code || !state) {
       throw new Error("promiseFxAOAuthToken: code and state are required.");
     }
 
     let payload = {
       code: code,
       state: state,
     };
-    return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => {
+    return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => {
       return JSON.parse(response.body);
     });
   },
 
   /**
    * Called once gFxAOAuthClient fires onComplete.
    *
    * @param {Deferred} deferred used to resolve or reject the gFxAOAuthClientPromise
@@ -837,16 +889,29 @@ this.MozLoopService = {
       return Services.prefs.getComplexValue("general.useragent.locale",
         Ci.nsISupportsString).data;
     } catch (ex) {
       return "en-US";
     }
   },
 
   /**
+   * Returns the callData for a specific callDataId
+   *
+   * The data was retrieved from the LoopServer via a GET/calls/<version> request
+   * triggered by an incoming message from the LoopPushServer.
+   *
+   * @param {int} loopCallId
+   * @return {callData} The callData or undefined if error.
+   */
+  getCallData: function(loopCallId) {
+    return MozLoopServiceInternal.callsData.data;
+  },
+
+  /**
    * Set any character preference under "loop.".
    *
    * @param {String} prefName The name of the pref without the preceding "loop."
    * @param {String} value The value to set.
    *
    * Any errors thrown by the Mozilla pref API are logged to the console.
    */
   setLoopCharPref: function(prefName, value) {
@@ -916,33 +981,44 @@ this.MozLoopService = {
       return Promise.resolve(gFxAOAuthTokenData);
     }
 
     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
     }).then(tokenData => {
       gFxAOAuthTokenData = tokenData;
       return tokenData;
+    }).then(tokenData => {
+      return gRegisteredDeferred.promise.then(Task.async(function*() {
+        if (gPushHandler.pushUrl) {
+          yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
+        } else {
+          throw new Error("No pushUrl for FxA registration");
+        }
+        return gFxAOAuthTokenData;
+      }));
     },
     error => {
       gFxAOAuthTokenData = null;
       throw error;
     });
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
+   *                                        One of the LOOP_SESSION_TYPE members.
    * @param {String} path The path to make the request to.
    * @param {String} method The request method, e.g. 'POST', 'GET'.
    * @param {Object} payloadObj An object which is converted to JSON and
    *                            transmitted with the request.
    * @returns {Promise}
    *        Returns a promise that resolves to the response of the API call,
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
-  hawkRequest: function(path, method, payloadObj) {
-    return MozLoopServiceInternal.hawkRequest(path, method, payloadObj);
+  hawkRequest: function(sessionType, path, method, payloadObj) {
+    return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj);
   },
 };
 Object.freeze(this.MozLoopService);
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -99,17 +99,16 @@ loop.Client = (function($) {
      * Internal handler for requesting a call url from the server.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
      * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
-     * @param  {String} simplepushUrl a registered Simple Push URL
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     _requestCallUrlInternal: function(nickname, cb) {
       this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname},
                                function (error, responseText) {
         if (error) {
           this._failureHandler(cb, error);
@@ -183,45 +182,12 @@ loop.Client = (function($) {
         if (err) {
           cb(err);
           return;
         }
 
         this._requestCallUrlInternal(nickname, cb);
       }.bind(this));
     },
-
-    /**
-     * Requests call information from the server for all calls since the
-     * given version.
-     *
-     * @param  {String} version the version identifier from the push
-     *                          notification
-     * @param  {Function} cb Callback(err, calls)
-     */
-    requestCallsInfo: function(version, cb) {
-      // XXX It is likely that we'll want to move some of this to whatever
-      // opens the chat window, but we'll need to decide on this in bug 1002418
-      if (!version) {
-        throw new Error("missing required parameter version");
-      }
-
-      this.mozLoop.hawkRequest("/calls?version=" + version, "GET", null,
-                               function (error, responseText) {
-        if (error) {
-          this._failureHandler(cb, error);
-          return;
-        }
-
-        try {
-          var callsData = JSON.parse(responseText);
-
-          cb(null, this._validate(callsData, expectedCallProperties));
-        } catch (err) {
-          console.log("Error requesting calls info", err);
-          cb(err);
-        }
-      }.bind(this));
-    }
   };
 
   return Client;
 })(jQuery);
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -152,17 +152,17 @@ loop.conversation = (function(OT, mozL10
    * Required options:
    * - {loop.shared.models.ConversationModel} conversation Conversation model.
    * - {loop.shared.models.NotificationCollection} notifications
    *
    * @type {loop.shared.router.BaseConversationRouter}
    */
   var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({
     routes: {
-      "incoming/:version": "incoming",
+      "incoming/:callId": "incoming",
       "call/accept": "accept",
       "call/decline": "decline",
       "call/ongoing": "conversation",
       "call/declineAndBlock": "declineAndBlock",
       "call/feedback": "feedback"
     },
 
     /**
@@ -177,53 +177,44 @@ loop.conversation = (function(OT, mozL10
      */
     endCall: function() {
       this.navigate("call/feedback", {trigger: true});
     },
 
     /**
      * Incoming call route.
      *
-     * @param {String} loopVersion The version from the push notification, set
-     *                             by the router from the URL.
+     * @param {String} callId  Identifier assigned by the LoopService
+     *                         to this incoming call.
      */
-    incoming: function(loopVersion) {
+    incoming: function(callId) {
       navigator.mozLoop.startAlerting();
-      this._conversation.set({loopVersion: loopVersion});
       this._conversation.once("accept", function() {
         this.navigate("call/accept", {trigger: true});
       }.bind(this));
       this._conversation.once("decline", function() {
         this.navigate("call/decline", {trigger: true});
       }.bind(this));
       this._conversation.once("declineAndBlock", function() {
         this.navigate("call/declineAndBlock", {trigger: true});
       }.bind(this));
       this._conversation.once("call:incoming", this.startCall, this);
       this._conversation.once("change:publishedStream", this._checkConnected, this);
       this._conversation.once("change:subscribedStream", this._checkConnected, this);
 
-      this._client.requestCallsInfo(loopVersion, function(err, sessionData) {
-        if (err) {
-          console.error("Failed to get the sessionData", err);
-          // XXX Not the ideal response, but bug 1047410 will be replacing
-          // this by better "call failed" UI.
-          this._notifications.errorL10n("cannot_start_call_session_not_ready");
-          return;
-        }
-
-        // XXX For incoming calls we might have more than one call queued.
-        // For now, we'll just assume the first call is the right information.
-        // We'll probably really want to be getting this data from the
-        // background worker on the desktop client.
-        // Bug 1032700 should fix this.
-        this._conversation.setIncomingSessionData(sessionData[0]);
-
-        this._setupWebSocketAndCallView();
-      }.bind(this));
+      var callData = navigator.mozLoop.getCallData(callId);
+      if (!callData) {
+        console.error("Failed to get the call data");
+        // XXX Not the ideal response, but bug 1047410 will be replacing
+        // this by better "call failed" UI.
+        this._notifications.errorL10n("cannot_start_call_session_not_ready");
+        return;
+      }
+      this._conversation.setIncomingSessionData(callData);
+      this._setupWebSocketAndCallView();
     },
 
     /**
      * Used to set up the web socket connection and navigate to the
      * call view if appropriate.
      */
     _setupWebSocketAndCallView: function() {
       this._websocket = new loop.CallConnectionWebSocket({
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -152,17 +152,17 @@ loop.conversation = (function(OT, mozL10
    * Required options:
    * - {loop.shared.models.ConversationModel} conversation Conversation model.
    * - {loop.shared.models.NotificationCollection} notifications
    *
    * @type {loop.shared.router.BaseConversationRouter}
    */
   var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({
     routes: {
-      "incoming/:version": "incoming",
+      "incoming/:callId": "incoming",
       "call/accept": "accept",
       "call/decline": "decline",
       "call/ongoing": "conversation",
       "call/declineAndBlock": "declineAndBlock",
       "call/feedback": "feedback"
     },
 
     /**
@@ -177,53 +177,44 @@ loop.conversation = (function(OT, mozL10
      */
     endCall: function() {
       this.navigate("call/feedback", {trigger: true});
     },
 
     /**
      * Incoming call route.
      *
-     * @param {String} loopVersion The version from the push notification, set
-     *                             by the router from the URL.
+     * @param {String} callId  Identifier assigned by the LoopService
+     *                         to this incoming call.
      */
-    incoming: function(loopVersion) {
+    incoming: function(callId) {
       navigator.mozLoop.startAlerting();
-      this._conversation.set({loopVersion: loopVersion});
       this._conversation.once("accept", function() {
         this.navigate("call/accept", {trigger: true});
       }.bind(this));
       this._conversation.once("decline", function() {
         this.navigate("call/decline", {trigger: true});
       }.bind(this));
       this._conversation.once("declineAndBlock", function() {
         this.navigate("call/declineAndBlock", {trigger: true});
       }.bind(this));
       this._conversation.once("call:incoming", this.startCall, this);
       this._conversation.once("change:publishedStream", this._checkConnected, this);
       this._conversation.once("change:subscribedStream", this._checkConnected, this);
 
-      this._client.requestCallsInfo(loopVersion, function(err, sessionData) {
-        if (err) {
-          console.error("Failed to get the sessionData", err);
-          // XXX Not the ideal response, but bug 1047410 will be replacing
-          // this by better "call failed" UI.
-          this._notifications.errorL10n("cannot_start_call_session_not_ready");
-          return;
-        }
-
-        // XXX For incoming calls we might have more than one call queued.
-        // For now, we'll just assume the first call is the right information.
-        // We'll probably really want to be getting this data from the
-        // background worker on the desktop client.
-        // Bug 1032700 should fix this.
-        this._conversation.setIncomingSessionData(sessionData[0]);
-
-        this._setupWebSocketAndCallView();
-      }.bind(this));
+      var callData = navigator.mozLoop.getCallData(callId);
+      if (!callData) {
+        console.error("Failed to get the call data");
+        // XXX Not the ideal response, but bug 1047410 will be replacing
+        // this by better "call failed" UI.
+        this._notifications.errorL10n("cannot_start_call_session_not_ready");
+        return;
+      }
+      this._conversation.setIncomingSessionData(callData);
+      this._setupWebSocketAndCallView();
     },
 
     /**
      * Used to set up the web socket connection and navigate to the
      * call view if appropriate.
      */
     _setupWebSocketAndCallView: function() {
       this._websocket = new loop.CallConnectionWebSocket({
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -13,20 +13,17 @@ loop.shared.models = (function(l10n) {
    * Conversation model.
    */
   var ConversationModel = Backbone.Model.extend({
     defaults: {
       connected:    false,         // Session connected flag
       ongoing:      false,         // Ongoing call flag
       callerId:     undefined,     // Loop caller id
       loopToken:    undefined,     // Loop conversation token
-      loopVersion:  undefined,     // Loop version for /calls/ information. This
-                                   // is the version received from the push
-                                   // notification and is used by the server to
-                                   // determine the pending calls
+      loopCallId:   undefined,     // LoopService id for incoming session
       sessionId:    undefined,     // OT session id
       sessionToken: undefined,     // OT session token
       apiKey:       undefined,     // OT api key
       callId:       undefined,     // The callId on the server
       progressURL:  undefined,     // The websocket url to use for progress
       websocketToken: undefined,   // The token to use for websocket auth, this is
                                    // stored as a hex string which is what the server
                                    // requires.
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -177,57 +177,10 @@ describe("loop.Client", function() {
         client.requestCallUrl("foo", callback);
 
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /Invalid data received/.test(err.message);
         }));
       });
     });
-
-    describe("#requestCallsInfo", function() {
-      it("should prevent launching a conversation when version is missing",
-        function() {
-          expect(function() {
-            client.requestCallsInfo();
-          }).to.Throw(Error, /missing required parameter version/);
-        });
-
-      it("should perform a get on /calls", function() {
-        client.requestCallsInfo(42, callback);
-
-        sinon.assert.calledOnce(hawkRequestStub);
-        sinon.assert.calledWith(hawkRequestStub,
-                                "/calls?version=42", "GET", null);
-
-      });
-
-      it("should request data for all calls", function() {
-        hawkRequestStub.callsArgWith(3, null,
-                                     '{"calls": [{"apiKey": "fake"}]}');
-
-        client.requestCallsInfo(42, callback);
-
-        sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
-      });
-
-      it("should send an error when the request fails", function() {
-        hawkRequestStub.callsArgWith(3, fakeErrorRes);
-
-        client.requestCallsInfo(42, callback);
-
-        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
-          return /400.*invalid token/.test(err.message);
-        }));
-      });
-
-      it("should send an error if the data is not valid", function() {
-        hawkRequestStub.callsArgWith(3, null, "{}");
-
-        client.requestCallsInfo(42, callback);
-
-        sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
-          return /Invalid data received/.test(err.message);
-        }));
-      });
-    });
   });
 });
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -27,16 +27,17 @@ describe("loop.conversation", function()
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub(),
       getLoopBoolPref: sandbox.stub(),
+      getCallData: sandbox.stub(),
       startAlerting: function() {},
       stopAlerting: function() {},
       ensureRegistered: function() {},
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
           platform: "test"
@@ -107,17 +108,16 @@ describe("loop.conversation", function()
     var conversation, client;
 
     beforeEach(function() {
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000,
       });
-      sandbox.stub(client, "requestCallsInfo");
       sandbox.spy(conversation, "setIncomingSessionData");
       sandbox.stub(conversation, "setOutgoingSessionData");
     });
 
     describe("Routes", function() {
       var router;
 
       beforeEach(function() {
@@ -152,57 +152,41 @@ describe("loop.conversation", function()
 
         it("should start alerting", function() {
           sandbox.stub(navigator.mozLoop, "startAlerting");
           router.incoming("fakeVersion");
 
           sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
         });
 
-        it("should set the loopVersion on the conversation model", function() {
-          router.incoming("fakeVersion");
-
-          expect(conversation.get("loopVersion")).to.equal("fakeVersion");
-        });
-
-        it("should call requestCallsInfo on the client",
+        it("should call getCallData on navigator.mozLoop",
           function() {
             router.incoming(42);
 
-            sinon.assert.calledOnce(client.requestCallsInfo);
-            sinon.assert.calledWith(client.requestCallsInfo, 42);
+            sinon.assert.calledOnce(navigator.mozLoop.getCallData);
+            sinon.assert.calledWith(navigator.mozLoop.getCallData, 42);
           });
 
-        it("should display an error if requestCallsInfo returns an error",
-          function(){
-            sandbox.stub(notifications, "errorL10n");
-            client.requestCallsInfo.callsArgWith(1, "failed");
-
-            router.incoming(42);
-
-            sinon.assert.calledOnce(notifications.errorL10n);
-          });
-
-        describe("requestCallsInfo successful", function() {
+        describe("getCallData successful", function() {
           var fakeSessionData, resolvePromise, rejectPromise;
 
           beforeEach(function() {
             fakeSessionData  = {
               sessionId:      "sessionId",
               sessionToken:   "sessionToken",
               apiKey:         "apiKey",
               callType:       "callType",
               callId:         "Hello",
               progressURL:    "http://progress.example.com",
               websocketToken: 123
             };
 
             sandbox.stub(router, "_setupWebSocketAndCallView");
 
-            client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
+            navigator.mozLoop.getCallData.returns(fakeSessionData);
           });
 
           it("should store the session data", function() {
             router.incoming("fakeVersion");
 
             sinon.assert.calledOnce(conversation.setIncomingSessionData);
             sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
                                            fakeSessionData);
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -2,27 +2,34 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test FxA logins with Loop.
  */
 
 "use strict";
 
-const gFxAOAuthTokenData = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gFxAOAuthTokenData;
+const {
+  LOOP_SESSION_TYPE,
+  gFxAOAuthTokenData
+} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
+
 const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
+const HAWK_TOKEN_LENGTH = 64;
 
 add_task(function* setup() {
   Services.prefs.setCharPref("loop.server", BASE_URL);
   Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
   registerCleanupFunction(function* () {
     info("cleanup time");
     yield promiseDeletedOAuthParams(BASE_URL);
     Services.prefs.clearUserPref("loop.server");
     Services.prefs.clearUserPref("services.push.serverURL");
+    Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST));
+    Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA));
   });
 });
 
 add_task(function* checkOAuthParams() {
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
@@ -156,33 +163,51 @@ add_task(function* basicAuthorizationAnd
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
     profile_uri: BASE_URL + "/profile",
     state: "state",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
 
+  info("registering");
+  mockPushHandler.pushUrl = "https://localhost/pushUrl/guest";
+  yield MozLoopService.register(mockPushHandler);
+  let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST);
+  let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join("");
+  ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token");
+
+  // Normally the same pushUrl would be registered but we change it in the test
+  // to be able to check for success on the second registration.
+  mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa";
+
   let tokenData = yield MozLoopService.logInToFxA();
   ise(tokenData.access_token, "code1_access_token", "Check access_token");
   ise(tokenData.scope, "profile", "Check scope");
   ise(tokenData.token_type, "bearer", "Check token_type");
+
+  let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
+  ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL");
+  prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
+  padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join("");
+  ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token");
 });
 
 add_task(function* loginWithParams401() {
   resetFxA();
   let params = {
     client_id: "client_id",
     content_uri: BASE_URL + "/content",
     oauth_uri: BASE_URL + "/oauth",
     profile_uri: BASE_URL + "/profile",
     state: "state",
     test_error: "params_401",
   };
   yield promiseOAuthParamsSetup(BASE_URL, params);
+  yield MozLoopService.register(mockPushHandler);
 
   let loginPromise = MozLoopService.logInToFxA();
   yield loginPromise.then(tokenData => {
     ok(false, "Promise should have rejected");
   },
   error => {
     ise(error.code, 401, "Check error code");
     ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -107,8 +107,51 @@ function promiseDeletedOAuthParams(baseU
               createInstance(Ci.nsIXMLHttpRequest);
   xhr.open("DELETE", baseURL + "/setup_params", true);
   xhr.addEventListener("load", () => deferred.resolve(xhr));
   xhr.addEventListener("error", deferred.reject);
   xhr.send();
 
   return deferred.promise;
 }
+
+/**
+ * Get the last registration on the test server.
+ */
+function promiseOAuthGetRegistration(baseURL) {
+  let deferred = Promise.defer();
+  let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+              createInstance(Ci.nsIXMLHttpRequest);
+  xhr.open("GET", baseURL + "/get_registration", true);
+  xhr.responseType = "json";
+  xhr.addEventListener("load", () => deferred.resolve(xhr));
+  xhr.addEventListener("error", deferred.reject);
+  xhr.send();
+
+  return deferred.promise;
+}
+
+/**
+ * This is used to fake push registration and notifications for
+ * MozLoopService tests. There is only one object created per test instance, as
+ * once registration has taken place, the object cannot currently be changed.
+ */
+let mockPushHandler = {
+  // This sets the registration result to be returned when initialize
+  // is called. By default, it is equivalent to success.
+  registrationResult: null,
+  pushUrl: undefined,
+
+  /**
+   * MozLoopPushHandler API
+   */
+  initialize: function(registerCallback, notificationCallback) {
+    registerCallback(this.registrationResult, this.pushUrl);
+    this._notificationCallback = notificationCallback;
+  },
+
+  /**
+   * Test-only API to simplify notifying a push notification result.
+   */
+  notify: function(version) {
+    this._notificationCallback(version);
+  }
+};
--- a/browser/components/loop/test/mochitest/loop_fxa.sjs
+++ b/browser/components/loop/test/mochitest/loop_fxa.sjs
@@ -3,37 +3,44 @@
 
 /**
  * This is a mock server that implements the FxA endpoints on the Loop server.
  */
 
 "use strict";
 
 const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"];
+const HAWK_TOKEN_LENGTH = 64;
 
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
 /**
  * Entry point for HTTP requests.
  */
 function handleRequest(request, response) {
   // Look at the query string but ignore past the encoded ? when deciding on the handler.
   switch (request.queryString.replace(/%3F.*/,"")) {
-    case "/setup_params":
+    case "/setup_params": // Test-only
       setup_params(request, response);
       return;
     case "/fxa-oauth/params":
       params(request, response);
       return;
     case encodeURIComponent("/oauth/authorization"):
       oauth_authorization(request, response);
       return;
     case "/fxa-oauth/token":
       token(request, response);
       return;
+    case "/registration":
+      registration(request, response);
+      return;
+    case "/get_registration": // Test-only
+      get_registration(request, response);
+      return;
   }
   response.setStatusLine(request.httpVersion, 404, "Not Found");
 }
 
 /**
  * POST /setup_params
  * DELETE /setup_params
  *
@@ -42,16 +49,17 @@ function handleRequest(request, response
  * For a POST the X-Params header should contain a JSON object with keys to set for /fxa-oauth/params.
  * A DELETE request will delete the stored parameters and should be run in a cleanup function to
  * avoid interfering with subsequen tests.
  */
 function setup_params(request, response) {
   response.setHeader("Content-Type", "text/plain", false);
   if (request.method == "DELETE") {
     setSharedState("/fxa-oauth/params", "");
+    setSharedState("/registration", "");
     response.write("Params deleted");
     return;
   }
   let params = JSON.parse(request.getHeader("X-Params"));
   if (!params) {
     response.setStatusLine(request.httpVersion, 400, "Bad Request");
     return;
   }
@@ -136,8 +144,35 @@ function token(request, response) {
   let tokenData = {
     access_token: payload.code + "_access_token",
     scope: "profile",
     token_type: "bearer",
   };
   response.setHeader("Content-Type", "application/json; charset=utf-8", false);
   response.write(JSON.stringify(tokenData, null, 2));
 }
+
+/**
+ * POST /registration
+ *
+ * Mock Loop registration endpoint which simply returns the simplePushURL with
+ * padding as the hawk session token.
+ */
+function registration(request, response) {
+  let body = NetUtil.readInputStreamToString(request.bodyInputStream,
+                                             request.bodyInputStream.available());
+  let payload = JSON.parse(body);
+  setSharedState("/registration", body);
+  let pushURL = payload.simplePushURL;
+  // Pad the pushURL with "X" to the token length to simulate a token
+  let padding = new Array(HAWK_TOKEN_LENGTH - pushURL.length).fill("X").join("");
+  response.setHeader("hawk-session-token", pushURL + padding, false);
+}
+
+/**
+ * GET /get_registration
+ *
+ * Used for testing purposes to check if registration succeeded by returning the POST body.
+ */
+function get_registration(request, response) {
+  response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+  response.write(getSharedState("/registration"));
+}
--- a/browser/components/loop/test/xpcshell/head.js
+++ b/browser/components/loop/test/xpcshell/head.js
@@ -22,16 +22,23 @@ const kEndPointUrl = "http://example.com
 const kUAID = "f47ac11b-58ca-4372-9567-0e02b2c3d479";
 
 // Fake loop server
 var loopServer;
 
 // Ensure loop is always enabled for tests
 Services.prefs.setBoolPref("loop.enabled", true);
 
+function hawkGetCallsRequest() {
+  let response = {body: JSON.stringify({calls: [{callId: 4444333221, websocketToken: "0deadbeef0"}]})},
+      // Call the first non-null then(resolve) function attached to the fakePromise.
+      fakePromise = {then: (resolve) => {return resolve ? resolve(response) : fakePromise;},
+                     catch: () => {return fakePromise;}};
+  return fakePromise;
+}
 
 function setupFakeLoopServer() {
   loopServer = new HttpServer();
   loopServer.start(-1);
 
   Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl);
 
   Services.prefs.setCharPref("loop.server",
--- a/browser/components/loop/test/xpcshell/test_loopservice_dnd.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_dnd.js
@@ -1,16 +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/. */
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
+let openChatOrig = Chat.open;
 
-let openChatOrig = Chat.open;
+const loopServiceModule = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 
 add_test(function test_get_do_not_disturb() {
   Services.prefs.setBoolPref("loop.do_not_disturb", false);
 
   do_check_false(MozLoopService.doNotDisturb);
 
   Services.prefs.setBoolPref("loop.do_not_disturb", true);
 
@@ -33,20 +34,25 @@ add_test(function test_do_not_disturb_di
   MozLoopService.doNotDisturb = false;
 
   MozLoopService.register(mockPushHandler).then(() => {
     let opened = false;
     Chat.open = function() {
       opened = true;
     };
 
+    let savedHawkClient = loopServiceModule.gHawkClient;
+    loopServiceModule.gHawkClient = {request: hawkGetCallsRequest};
+
     mockPushHandler.notify(1);
 
     do_check_true(opened, "should open a chat window");
 
+    loopServiceModule.gHawkClient = savedHawkClient;
+
     run_next_test();
   });
 });
 
 add_task(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
   MozLoopService.doNotDisturb = true;
 
   // We registered in the previous test, so no need to do that on this one.
--- a/browser/components/loop/test/xpcshell/test_loopservice_notification.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_notification.js
@@ -2,32 +2,39 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 
 let openChatOrig = Chat.open;
 
+const loopServiceModule = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
+
 add_test(function test_openChatWindow_on_notification() {
   Services.prefs.setCharPref("loop.seenToS", "unseen");
 
   MozLoopService.register(mockPushHandler).then(() => {
     let opened = false;
     Chat.open = function() {
       opened = true;
     };
 
+    let savedHawkClient = loopServiceModule.gHawkClient;
+    loopServiceModule.gHawkClient = {request: hawkGetCallsRequest};
+
     mockPushHandler.notify(1);
 
     do_check_true(opened, "should open a chat window");
 
     do_check_eq(Services.prefs.getCharPref("loop.seenToS"), "seen",
                 "should set the pref to 'seen'");
 
+    loopServiceModule.gHawkClient = savedHawkClient;
+
     run_next_test();
   });
 });
 
 function run_test()
 {
   setupFakeLoopServer();
 
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -55,32 +55,25 @@ var gMainPane = {
 #endif
 
     // set up the "use current page" label-changing listener
     this._updateUseCurrentButton();
     window.addEventListener("focus", this._updateUseCurrentButton.bind(this), false);
 
     this.updateBrowserStartupLastSession();
 
-    // Notify observers that the UI is now ready
-    Components.classes["@mozilla.org/observer-service;1"]
-              .getService(Components.interfaces.nsIObserverService)
-              .notifyObservers(window, "main-pane-loaded", null);
-
 #ifdef XP_WIN
     // Functionality for "Show tabs in taskbar" on Windows 7 and up.
-
     try {
       let sysInfo = Cc["@mozilla.org/system-info;1"].
                     getService(Ci.nsIPropertyBag2);
       let ver = parseFloat(sysInfo.getProperty("version"));
       let showTabsInTaskbar = document.getElementById("showTabsInTaskbar");
       showTabsInTaskbar.hidden = ver < 6.1;
     } catch (ex) {}
-
 #endif
 
     setEventListener("browser.privatebrowsing.autostart", "change",
                      gMainPane.updateBrowserStartupLastSession);
     setEventListener("browser.download.dir", "change",
                      gMainPane.displayDownloadDirPref);
 #ifdef HAVE_SHELL_SERVICE
     setEventListener("setDefaultButton", "command",
@@ -89,16 +82,21 @@ var gMainPane = {
     setEventListener("useCurrent", "command",
                      gMainPane.setHomePageToCurrent);
     setEventListener("useBookmark", "command",
                      gMainPane.setHomePageToBookmark);
     setEventListener("restoreDefaultHomePage", "command",
                      gMainPane.restoreDefaultHomePage);
     setEventListener("chooseFolder", "command",
                      gMainPane.chooseFolder);
+
+    // Notify observers that the UI is now ready
+    Components.classes["@mozilla.org/observer-service;1"]
+              .getService(Components.interfaces.nsIObserverService)
+              .notifyObservers(window, "main-pane-loaded", null);
   },
 
   // HOME PAGE
 
   /*
    * Preferences:
    *
    * browser.startup.homepage
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -114,8 +114,10 @@ browser.jar:
     content/browser/devtools/graphs-frame.xhtml                        (shared/widgets/graphs-frame.xhtml)
     content/browser/devtools/spectrum-frame.xhtml                      (shared/widgets/spectrum-frame.xhtml)
     content/browser/devtools/spectrum.css                              (shared/widgets/spectrum.css)
     content/browser/devtools/cubic-bezier-frame.xhtml                  (shared/widgets/cubic-bezier-frame.xhtml)
     content/browser/devtools/cubic-bezier.css                          (shared/widgets/cubic-bezier.css)
     content/browser/devtools/eyedropper.xul                            (eyedropper/eyedropper.xul)
     content/browser/devtools/eyedropper/crosshairs.css                 (eyedropper/crosshairs.css)
     content/browser/devtools/eyedropper/nocursor.css                   (eyedropper/nocursor.css)
+    content/browser/devtools/timeline/timeline.xul                     (timeline/timeline.xul)
+    content/browser/devtools/timeline/timeline.js                      (timeline/timeline.js)
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -26,46 +26,49 @@ loader.lazyGetter(this, "OptionsPanel", 
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel);
 loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel);
 loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
 loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel);
 loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/webaudioeditor/panel").WebAudioEditorPanel);
 loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel").ProfilerPanel);
+loader.lazyGetter(this, "TimelinePanel", () => require("devtools/timeline/panel").TimelinePanel);
 loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
+loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel);
 loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
-loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel);
 
 // Strings
 const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
 const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
+const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
 const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
 const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
 const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
 const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties";
 const webAudioEditorProps = "chrome://browser/locale/devtools/webaudioeditor.properties";
-const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
 const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
+const timelineProps = "chrome://browser/locale/devtools/timeline.properties";
 const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
+const storageProps = "chrome://browser/locale/devtools/storage.properties";
 const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
-const storageProps = "chrome://browser/locale/devtools/storage.properties";
 
 loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
+loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
 loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
 loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
 loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
 loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps));
 loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps));
 loader.lazyGetter(this, "webAudioEditorStrings", () => Services.strings.createBundle(webAudioEditorProps));
 loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
-loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
+loader.lazyGetter(this, "timelineStrings", () => Services.strings.createBundle(timelineProps));
 loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
+loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps));
 loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
-loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps));
 
 let Tools = {};
 exports.Tools = Tools;
 
 // Definitions
 Tools.options = {
   id: "options",
   ordinal: 0,
@@ -73,19 +76,21 @@ Tools.options = {
   icon: "chrome://browser/skin/devtools/tool-options.svg",
   invertIconForLightTheme: true,
   bgTheme: "theme-body",
   label: l10n("options.label", toolboxStrings),
   iconOnly: true,
   panelLabel: l10n("options.panelLabel", toolboxStrings),
   tooltip: l10n("optionsButton.tooltip", toolboxStrings),
   inMenu: false,
+
   isTargetSupported: function(target) {
     return true;
   },
+
   build: function(iframeWindow, toolbox) {
     return new OptionsPanel(iframeWindow, toolbox);
   }
 }
 
 Tools.webConsole = {
   id: "webconsole",
   key: l10n("cmd.commandkey", webConsoleStrings),
@@ -108,16 +113,17 @@ Tools.webConsole = {
       return toolbox.focusConsoleInput();
 
     panel.focusInput();
   },
 
   isTargetSupported: function(target) {
     return true;
   },
+
   build: function(iframeWindow, toolbox) {
     return new WebConsolePanel(iframeWindow, toolbox);
   }
 };
 
 Tools.inspector = {
   id: "inspector",
   accesskey: l10n("inspector.accesskey", inspectorStrings),
@@ -225,39 +231,43 @@ Tools.canvasDebugger = {
   ordinal: 6,
   visibilityswitch: "devtools.canvasdebugger.enabled",
   icon: "chrome://browser/skin/devtools/tool-styleeditor.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/canvasdebugger.xul",
   label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings),
   panelLabel: l10n("ToolboxCanvasDebugger.panelLabel", canvasDebuggerStrings),
   tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings),
+
   // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox
   // (bug 1047520).
   isTargetSupported: function(target) {
     return !target.isAddon && !target.chrome;
   },
+
   build: function (iframeWindow, toolbox) {
     return new CanvasDebuggerPanel(iframeWindow, toolbox);
   }
 };
 
 Tools.webAudioEditor = {
   id: "webaudioeditor",
   ordinal: 10,
   visibilityswitch: "devtools.webaudioeditor.enabled",
   icon: "chrome://browser/skin/devtools/tool-webaudio.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/webaudioeditor.xul",
   label: l10n("ToolboxWebAudioEditor1.label", webAudioEditorStrings),
   panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel", webAudioEditorStrings),
   tooltip: l10n("ToolboxWebAudioEditor1.tooltip", webAudioEditorStrings),
+
   isTargetSupported: function(target) {
     return !target.isAddon;
   },
+
   build: function(iframeWindow, toolbox) {
     return new WebAudioEditorPanel(iframeWindow, toolbox);
   }
 };
 
 Tools.jsprofiler = {
   id: "jsprofiler",
   accesskey: l10n("profiler.accesskey", profilerStrings),
@@ -279,21 +289,42 @@ Tools.jsprofiler = {
     return !target.isAddon && (!target.isApp || target.form.profilerActor);
   },
 
   build: function (frame, target) {
     return new ProfilerPanel(frame, target);
   }
 };
 
+Tools.timeline = {
+  id: "timeline",
+  ordinal: 8,
+  visibilityswitch: "devtools.timeline.enabled",
+  icon: "chrome://browser/skin/devtools/tool-network.svg",
+  invertIconForLightTheme: true,
+  url: "chrome://browser/content/devtools/timeline/timeline.xul",
+  label: l10n("timeline.label", timelineStrings),
+  panelLabel: l10n("timeline.panelLabel", timelineStrings),
+  tooltip: l10n("timeline.tooltip", timelineStrings),
+
+  isTargetSupported: function(target) {
+    return !target.isAddon;
+  },
+
+  build: function (iframeWindow, toolbox) {
+    let panel = new TimelinePanel(iframeWindow, toolbox);
+    return panel.open();
+  }
+};
+
 Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey", netMonitorStrings),
   key: l10n("netmonitor.commandkey", netMonitorStrings),
-  ordinal: 8,
+  ordinal: 9,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   visibilityswitch: "devtools.netmonitor.enabled",
   icon: "chrome://browser/skin/devtools/tool-network.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/netmonitor.xul",
   label: l10n("netmonitor.label", netMonitorStrings),
   panelLabel: l10n("netmonitor.panelLabel", netMonitorStrings),
   tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
@@ -307,17 +338,17 @@ Tools.netMonitor = {
   build: function(iframeWindow, toolbox) {
     return new NetMonitorPanel(iframeWindow, toolbox);
   }
 };
 
 Tools.storage = {
   id: "storage",
   key: l10n("storage.commandkey", storageStrings),
-  ordinal: 9,
+  ordinal: 10,
   accesskey: l10n("storage.accesskey", storageStrings),
   modifiers: "shift",
   visibilityswitch: "devtools.storage.enabled",
   icon: "chrome://browser/skin/devtools/tool-storage.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/storage.xul",
   label: l10n("storage.label", storageStrings),
   menuLabel: l10n("storage.menuLabel", storageStrings),
@@ -332,17 +363,17 @@ Tools.storage = {
 
   build: function(iframeWindow, toolbox) {
     return new StoragePanel(iframeWindow, toolbox);
   }
 };
 
 Tools.scratchpad = {
   id: "scratchpad",
-  ordinal: 10,
+  ordinal: 11,
   visibilityswitch: "devtools.scratchpad.enabled",
   icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/scratchpad.xul",
   label: l10n("scratchpad.label", scratchpadStrings),
   panelLabel: l10n("scratchpad.panelLabel", scratchpadStrings),
   tooltip: l10n("scratchpad.tooltip", scratchpadStrings),
   inMenu: false,
@@ -362,16 +393,17 @@ let defaultTools = [
   Tools.webConsole,
   Tools.inspector,
   Tools.jsdebugger,
   Tools.styleEditor,
   Tools.shaderEditor,
   Tools.canvasDebugger,
   Tools.webAudioEditor,
   Tools.jsprofiler,
+  Tools.timeline,
   Tools.netMonitor,
   Tools.storage,
   Tools.scratchpad
 ];
 
 exports.defaultTools = defaultTools;
 
 for (let definition of defaultTools) {
--- a/browser/devtools/moz.build
+++ b/browser/devtools/moz.build
@@ -8,30 +8,31 @@ DIRS += [
     'app-manager',
     'canvasdebugger',
     'commandline',
     'debugger',
     'eyedropper',
     'fontinspector',
     'framework',
     'inspector',
-    'projecteditor',
     'layoutview',
     'markupview',
     'netmonitor',
     'profiler',
+    'projecteditor',
     'responsivedesign',
     'scratchpad',
     'shadereditor',
     'shared',
     'sourceeditor',
     'storage',
     'styleeditor',
     'styleinspector',
     'tilt',
+    'timeline',
     'webaudioeditor',
     'webconsole',
     'webide',
 ]
 
 EXTRA_COMPONENTS += [
     'devtools-clhandler.js',
     'devtools-clhandler.manifest',
--- a/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js
+++ b/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js
@@ -1,9 +1,9 @@
-/* Any copyright is dedicated to the Public Domain.
+s/* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the retrieved profiler data samples are correctly filtered and
  * normalized before passed to consumers.
  */
 
 const WAIT_TIME = 1000; // ms
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -16,17 +16,18 @@ support-files =
 [browser_cubic-bezier-02.js]
 [browser_cubic-bezier-03.js]
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
-[browser_graphs-07.js]
+[browser_graphs-07a.js]
+[browser_graphs-07b.js]
 [browser_graphs-08.js]
 [browser_graphs-09.js]
 [browser_graphs-10a.js]
 [browser_graphs-10b.js]
 [browser_graphs-11a.js]
 [browser_graphs-11b.js]
 [browser_graphs-12.js]
 [browser_graphs-13.js]
rename from browser/devtools/shared/test/browser_graphs-07.js
rename to browser/devtools/shared/test/browser_graphs-07a.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_graphs-07b.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests if selections can't be added via clicking, while not allowed.
+
+const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
+let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
+let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
+let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
+let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let [host, win, doc] = yield createHost();
+  let graph = new LineGraphWidget(doc.body, "fps");
+  yield graph.once("ready");
+
+  testGraph(graph);
+
+  graph.destroy();
+  host.destroy();
+}
+
+function testGraph(graph) {
+  graph.setData(TEST_DATA);
+  graph.selectionEnabled = false;
+
+  info("Attempting to make a selection.");
+
+  dragStart(graph, 300);
+  is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+    "The graph shouldn't have a selection (1).");
+
+  hover(graph, 400);
+  is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+    "The graph shouldn't have a selection (2).");
+
+  dragStop(graph, 500);
+  is(graph.hasSelection() || graph.hasSelectionInProgress(), false,
+    "The graph shouldn't have a selection (3).");
+}
+
+// EventUtils just doesn't work!
+
+function hover(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+}
+
+function dragStart(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseDown({ clientX: x, clientY: y });
+}
+
+function dragStop(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseUp({ clientX: x, clientY: y });
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -5,17 +5,22 @@
 
 const Cu = Components.utils;
 
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 
-this.EXPORTED_SYMBOLS = ["LineGraphWidget", "BarGraphWidget", "CanvasGraphUtils"];
+this.EXPORTED_SYMBOLS = [
+  "AbstractCanvasGraph",
+  "LineGraphWidget",
+  "BarGraphWidget",
+  "CanvasGraphUtils"
+];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 
 // Generic constants.
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075;
@@ -490,16 +495,22 @@ AbstractCanvasGraph.prototype = {
    * via a click+drag operation.
    * @return boolean
    */
   hasSelectionInProgress: function() {
     return this._selection.start != null && this._selection.end == null;
   },
 
   /**
+   * Specifies whether or not mouse selection is allowed.
+   * @type boolean
+   */
+  selectionEnabled: true,
+
+  /**
    * Sets the selection bounds.
    * Use `dropCursor` to hide the cursor.
    *
    * @param object cursor
    *        The cursor's { x, y } position.
    */
   setCursor: function(cursor) {
     if (!cursor || cursor.x == null || cursor.y == null) {
@@ -950,16 +961,19 @@ AbstractCanvasGraph.prototype = {
    */
   _onMouseDown: function(e) {
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
+        if (!this.selectionEnabled) {
+          break;
+        }
         this._selection.start = mouseX;
         this._selection.end = null;
         this.emit("selecting");
         break;
 
       case "hovering-selection-start-boundary":
         this._selectionResizer.margin = "start";
         break;
@@ -985,16 +999,19 @@ AbstractCanvasGraph.prototype = {
    */
   _onMouseUp: function(e) {
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
+        if (!this.selectionEnabled) {
+          break;
+        }
         if (this.getSelectionWidth() < 1) {
           let region = this.getHoveredRegion();
           if (region) {
             this._selection.start = region.start;
             this._selection.end = region.end;
             this.emit("selecting");
           } else {
             this._selection.start = null;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/moz.build
@@ -0,0 +1,13 @@
+# vim: set filetype=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/.
+
+EXTRA_JS_MODULES.devtools.timeline += [
+    'panel.js',
+    'widgets/global.js',
+    'widgets/overview.js',
+    'widgets/waterfall.js'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/panel.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+"use strict";
+
+const { Cc, Ci, Cu, Cr } = require("chrome");
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+loader.lazyRequireGetter(this, "promise");
+loader.lazyRequireGetter(this, "EventEmitter",
+  "devtools/toolkit/event-emitter");
+
+loader.lazyRequireGetter(this, "TimelineFront",
+  "devtools/server/actors/timeline", true);
+
+function TimelinePanel(iframeWindow, toolbox) {
+  this.panelWin = iframeWindow;
+  this._toolbox = toolbox;
+
+  EventEmitter.decorate(this);
+};
+
+exports.TimelinePanel = TimelinePanel;
+
+TimelinePanel.prototype = {
+  /**
+   * Open is effectively an asynchronous constructor.
+   *
+   * @return object
+   *         A promise that is resolved when the timeline completes opening.
+   */
+  open: Task.async(function*() {
+    // Local debugging needs to make the target remote.
+    yield this.target.makeRemote();
+
+    this.panelWin.gToolbox = this._toolbox;
+    this.panelWin.gTarget = this.target;
+    this.panelWin.gFront = new TimelineFront(this.target.client, this.target.form);
+    yield this.panelWin.startupTimeline();
+
+    this.isReady = true;
+    this.emit("ready");
+    return this;
+  }),
+
+  // DevToolPanel API
+
+  get target() this._toolbox.target,
+
+  destroy: Task.async(function*() {
+    // Make sure this panel is not already destroyed.
+    if (this._destroyed) {
+      return;
+    }
+
+    yield this.panelWin.shutdownTimeline();
+    this.emit("destroyed");
+    this._destroyed = true;
+  })
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+skip-if = e10s # Bug 1065355 - devtools tests disabled with e10s
+subsuite = devtools
+support-files =
+  doc_simple-test.html
+  head.js
+
+[browser_timeline_aaa_run_first_leaktest.js]
+[browser_timeline_blueprint.js]
+[browser_timeline_overview-initial-selection-01.js]
+[browser_timeline_overview-initial-selection-02.js]
+[browser_timeline_overview-update.js]
+[browser_timeline_panels.js]
+[browser_timeline_recording.js]
+[browser_timeline_waterfall-background.js]
+[browser_timeline_waterfall-generic.js]
+[browser_timeline_waterfall-styles.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+
+  ok(target, "Should have a target available.");
+  ok(debuggee, "Should have a debuggee available.");
+  ok(panel, "Should have a panel available.");
+
+  ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window.");
+  ok(panel.panelWin.gTarget, "Should have a target reference on the panel window.");
+  ok(panel.panelWin.gFront, "Should have a front reference on the panel window.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_blueprint.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline blueprint has a correct structure.
+ */
+
+function test() {
+  let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global");
+
+  ok(TIMELINE_BLUEPRINT,
+    "A timeline blueprint should be available.");
+
+  ok(Object.keys(TIMELINE_BLUEPRINT).length,
+    "The timeline blueprint has at least one entry.");
+
+  for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) {
+    ok("group" in value,
+      "Each entry in the timeline blueprint contains a `group` key.");
+    ok("fill" in value,
+      "Each entry in the timeline blueprint contains a `fill` key.");
+    ok("stroke" in value,
+      "Each entry in the timeline blueprint contains a `stroke` key.");
+    ok("label" in value,
+      "Each entry in the timeline blueprint contains a `label` key.");
+  }
+
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the overview has an initial selection when recording has finished
+ * and there is data available.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 10)),
+    "The overview graph was updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  let markers = TimelineController.getMarkers();
+  let selection = TimelineView.overview.getSelection();
+
+  is((selection.start) | 0,
+     (markers[0].start * TimelineView.overview.dataScaleX) | 0,
+    "The initial selection start is correct.");
+
+  is((selection.end - selection.start) | 0,
+     (selectionRatio * TimelineView.overview.width) | 0,
+    "The initial selection end is correct.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the overview has no initial selection when recording has finished
+ * and there is no data available.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+  let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  yield TimelineController._stopRecordingAndDiscardData();
+  ok(true, "Recording has ended.");
+
+  let markers = TimelineController.getMarkers();
+  let selection = TimelineView.overview.getSelection();
+
+  is(markers.length, 0,
+    "There are no markers available.");
+  is(selection.start, null,
+    "The initial selection start is correct.");
+  is(selection.end, null,
+    "The initial selection end is correct.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_overview-update.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the overview graph is continuously updated.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel("about:blank");
+  let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+  yield DevToolsUtils.waitForTime(1000);
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  ok("selectionEnabled" in TimelineView.overview,
+    "The selection should not be enabled for the overview graph (1).");
+  is(TimelineView.overview.selectionEnabled, false,
+    "The selection should not be enabled for the overview graph (2).");
+  is(TimelineView.overview.hasSelection(), false,
+    "The overview graph shouldn't have a selection before recording.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 10)),
+    "The overview graph was updated a bunch of times.");
+
+  ok("selectionEnabled" in TimelineView.overview,
+    "The selection should still not be enabled for the overview graph (3).");
+  is(TimelineView.overview.selectionEnabled, false,
+    "The selection should still not be enabled for the overview graph (4).");
+  is(TimelineView.overview.hasSelection(), false,
+    "The overview graph should not have a selection while recording.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  is(TimelineController.getMarkers().length, 0,
+    "There are no markers available.");
+  is(TimelineView.overview.selectionEnabled, true,
+    "The selection should now be enabled for the overview graph.");
+  is(TimelineView.overview.hasSelection(), false,
+    "The overview graph should not have a selection after recording.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_panels.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline panels are correctly shown and hidden when
+ * recording starts and stops.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { $, EVENTS } = panel.panelWin;
+
+  is($("#record-button").hasAttribute("checked"), false,
+    "The record button should not be checked yet.");
+  is($("#timeline-pane").selectedPanel, $("#empty-notice"),
+    "An empty notice is initially displayed instead of the waterfall view.");
+
+  let whenRecStarted = panel.panelWin.once(EVENTS.RECORDING_STARTED);
+  EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin);
+  yield whenRecStarted;
+
+  ok(true, "Recording has started.");
+
+  is($("#record-button").getAttribute("checked"), "true",
+    "The record button should be checked now.");
+  is($("#timeline-pane").selectedPanel, $("#recording-notice"),
+    "A recording notice is now displayed instead of the waterfall view.");
+
+  let whenRecEnded = panel.panelWin.once(EVENTS.RECORDING_ENDED);
+  EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin);
+  yield whenRecEnded;
+
+  ok(true, "Recording has ended.");
+
+  is($("#record-button").hasAttribute("checked"), false,
+    "The record button should be unchecked again.");
+  is($("#timeline-pane").selectedPanel, $("#timeline-waterfall"),
+    "A waterfall view is now displayed.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_recording.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the timeline can properly start and stop a recording.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { gFront, TimelineController } = panel.panelWin;
+
+  is((yield gFront.isRecording()), false,
+    "The timeline actor should not be recording when the tool starts.");
+  is(TimelineController.getMarkers().length, 0,
+    "There should be no markers available when the tool starts.");
+
+  yield TimelineController.toggleRecording();
+
+  is((yield gFront.isRecording()), true,
+    "The timeline actor should be recording now.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available now.");
+
+  ok("startTime" in TimelineController.getMarkers(),
+    "A `startTime` field was set on the markers array.");
+  ok("endTime" in TimelineController.getMarkers(),
+    "An `endTime` field was set on the markers array.");
+  ok(TimelineController.getMarkers().endTime >
+     TimelineController.getMarkers().startTime,
+    "Some time has passed since the recording started.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-background.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall background is a 1px high canvas stretching across
+ * the container bounds.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 0)),
+    "The overview graph was updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  // Test the waterfall background.
+
+  let parentWidth = $("#timeline-waterfall").getBoundingClientRect().width;
+  let waterfallWidth = TimelineView.waterfall._waterfallWidth;
+  let sidebarWidth = 150; // px
+  is(waterfallWidth, parentWidth - sidebarWidth,
+    "The waterfall width is correct.")
+
+  ok(TimelineView.waterfall._canvas,
+    "A canvas should be created after the recording ended.");
+  ok(TimelineView.waterfall._ctx,
+    "A 2d context should be created after the recording ended.");
+
+  is(TimelineView.waterfall._canvas.width, waterfallWidth,
+    "The canvas width is correct.");
+  is(TimelineView.waterfall._canvas.height, 1,
+    "The canvas height is correct.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall is properly built after finishing a recording.
+ */
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { $, $$, EVENTS, TimelineController } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 0)),
+    "The overview graph was updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  // Test the header container.
+
+  ok($(".timeline-header-container"),
+    "A header container should have been created.");
+
+  // Test the header sidebar (left).
+
+  ok($(".timeline-header-sidebar"),
+    "A header sidebar node should have been created.");
+  ok($(".timeline-header-sidebar > .timeline-header-name"),
+    "A header name label should have been created inside the sidebar.");
+
+  // Test the header ticks (right).
+
+  ok($(".timeline-header-ticks"),
+    "A header ticks node should have been created.");
+  ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0,
+    "Some header tick labels should have been created inside the tick node.");
+
+  // Test the markers container.
+
+  ok($(".timeline-marker-container"),
+    "A marker container should have been created.");
+
+  // Test the markers sidebar (left).
+
+  ok($$(".timeline-marker-sidebar").length,
+    "Some marker sidebar nodes should have been created.");
+  ok($$(".timeline-marker-sidebar > .timeline-marker-bullet").length,
+    "Some marker color bullets should have been created inside the sidebar.");
+  ok($$(".timeline-marker-sidebar > .timeline-marker-name").length,
+    "Some marker name labels should have been created inside the sidebar.");
+
+  // Test the markers waterfall (right).
+
+  ok($$(".timeline-marker-waterfall").length,
+    "Some marker waterfall nodes should have been created.");
+  ok($$(".timeline-marker-waterfall > .timeline-marker-bar").length,
+    "Some marker color bars should have been created inside the waterfall.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall is properly built after making a selection
+ * and the child nodes are styled correctly.
+ */
+
+var gRGB_TO_HSL = {
+ "rgb(193, 132, 214)": "hsl(285,50%,68%)",
+ "rgb(152, 61, 183)": "hsl(285,50%,48%)",
+ "rgb(161, 223, 138)": "hsl(104,57%,71%)",
+ "rgb(96, 201, 58)": "hsl(104,57%,51%)",
+ "rgb(240, 195, 111)": "hsl(39,82%,69%)",
+ "rgb(227, 155, 22)": "hsl(39,82%,49%)",
+};
+
+let test = Task.async(function*() {
+  let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL);
+  let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global");
+  let { $, $$, EVENTS, TimelineController } = panel.panelWin;
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 0)),
+    "The overview graph was updated a bunch of times.");
+  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+    "There are some markers available.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  // Test the table sidebars.
+
+  for (let sidebar of [
+    ...$$(".timeline-header-sidebar"),
+    ...$$(".timeline-marker-sidebar")
+  ]) {
+    is(sidebar.getAttribute("width"), "150",
+      "The table's sidebar width is correct.");
+  }
+
+  // Test the table ticks.
+
+  for (let tick of $$(".timeline-header-tick")) {
+    ok(tick.getAttribute("value").match(/^\d+ ms$/),
+      "The table's timeline ticks appear to have correct labels.");
+    ok(tick.style.transform.match(/^translateX\(.*px\)$/),
+      "The table's timeline ticks appear to have proper translations.");
+  }
+
+  // Test the marker bullets.
+
+  for (let bullet of $$(".timeline-marker-bullet")) {
+    let type = bullet.getAttribute("type");
+
+    ok(type in TIMELINE_BLUEPRINT,
+      "The bullet type is present in the timeline blueprint.");
+    is(gRGB_TO_HSL[bullet.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill,
+      "The bullet's background color is correct.");
+    is(gRGB_TO_HSL[bullet.style.borderColor], TIMELINE_BLUEPRINT[type].stroke,
+      "The bullet's border color is correct.");
+  }
+
+  // Test the marker bars.
+
+  for (let bar of $$(".timeline-marker-bar")) {
+    let type = bar.getAttribute("type");
+
+    ok(type in TIMELINE_BLUEPRINT,
+      "The bar type is present in the timeline blueprint.");
+    is(gRGB_TO_HSL[bar.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill,
+      "The bar's background color is correct.");
+    is(gRGB_TO_HSL[bar.style.borderColor], TIMELINE_BLUEPRINT[type].stroke,
+      "The bar's border color is correct.");
+
+    ok(bar.getAttribute("width") > 0,
+      "The bar appears to have a proper width.");
+    ok(bar.style.transform.match(/^translateX\(.*px\)$/),
+      "The bar appears to have proper translations.");
+  }
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/doc_simple-test.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Timeline test page</title>
+  </head>
+
+  <body>
+    <script type="text/javascript">
+      function test() {
+        var a = "Hello world!";
+        document.body.style.backgroundColor = "rgba(" +
+          ((Math.random() * 64)|0) + "," +
+          ((Math.random() * 16)|0) + "," +
+          ((Math.random() * 16)|0) + ",1)";
+      }
+
+      // Prevent this script from being garbage collected.
+      window.setInterval(test, 1);
+    </script>
+  </body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/head.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+// Disable logging for all the tests. Both the debugger server and frontend will
+// be affected by this pref.
+let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+Services.prefs.setBoolPref("devtools.debugger.log", false);
+
+// Enable the tool while testing.
+let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled");
+Services.prefs.setBoolPref("devtools.timeline.enabled", true);
+
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
+let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+let TargetFactory = devtools.TargetFactory;
+let Toolbox = devtools.Toolbox;
+
+const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/";
+const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html";
+
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+registerCleanupFunction(() => {
+  info("finish() was called, cleaning up...");
+  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
+  Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled);
+});
+
+function addTab(url) {
+  info("Adding tab: " + url);
+
+  let deferred = promise.defer();
+  let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+  let linkedBrowser = tab.linkedBrowser;
+
+  linkedBrowser.addEventListener("load", function onLoad() {
+    linkedBrowser.removeEventListener("load", onLoad, true);
+    info("Tab added and finished loading: " + url);
+    deferred.resolve(tab);
+  }, true);
+
+  return deferred.promise;
+}
+
+function removeTab(tab) {
+  info("Removing tab.");
+
+  let deferred = promise.defer();
+  let tabContainer = gBrowser.tabContainer;
+
+  tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+    tabContainer.removeEventListener("TabClose", onClose, false);
+    info("Tab removed and finished closing.");
+    deferred.resolve();
+  }, false);
+
+  gBrowser.removeTab(tab);
+  return deferred.promise;
+}
+
+/**
+ * Spawns a new tab and starts up a toolbox with the timeline panel
+ * automatically selected.
+ *
+ * Must be used within a task.
+ *
+ * @param string url
+ *        The location of the new tab to spawn.
+ * @return object
+ *         A promise resolved once the timeline is initialized, with the
+ *         [target, debuggee, panel] instances.
+ */
+function* initTimelinePanel(url) {
+  info("Initializing a timeline pane.");
+
+  let tab = yield addTab(url);
+  let target = TargetFactory.forTab(tab);
+  let debuggee = target.window.wrappedJSObject;
+
+  yield target.makeRemote();
+
+  let toolbox = yield gDevTools.showToolbox(target, "timeline");
+  let panel = toolbox.getCurrentPanel();
+  return [target, debuggee, panel];
+}
+
+/**
+ * Closes a tab and destroys the toolbox holding a timeline panel.
+ *
+ * Must be used within a task.
+ *
+ * @param object panel
+ *        The timeline panel, created by the toolbox.
+ * @return object
+ *         A promise resolved once the timeline, toolbox and debuggee tab
+ *         are destroyed.
+ */
+function* teardown(panel) {
+  info("Destroying the specified timeline.");
+
+  let tab = panel.target.tab;
+  yield panel._toolbox.destroy();
+  yield removeTab(tab);
+}
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ *        Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ *        How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+  if (predicate()) {
+    return promise.resolve(true);
+  }
+  let deferred = promise.defer();
+  setTimeout(function() {
+    waitUntil(predicate).then(() => deferred.resolve(true));
+  }, interval);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/timeline.js
@@ -0,0 +1,281 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+
+devtools.lazyRequireGetter(this, "promise");
+devtools.lazyRequireGetter(this, "EventEmitter",
+  "devtools/toolkit/event-emitter");
+
+devtools.lazyRequireGetter(this, "Overview",
+  "devtools/timeline/overview", true);
+devtools.lazyRequireGetter(this, "Waterfall",
+  "devtools/timeline/waterfall", true);
+
+devtools.lazyImporter(this, "PluralForm",
+  "resource://gre/modules/PluralForm.jsm");
+
+const OVERVIEW_UPDATE_INTERVAL = 200;
+const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15;
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+  // When a recording is started or stopped, via the `stopwatch` button.
+  RECORDING_STARTED: "Timeline:RecordingStarted",
+  RECORDING_ENDED: "Timeline:RecordingEnded",
+
+  // When the overview graph is populated with new markers.
+  OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
+
+  // When the waterfall view is populated with new markers.
+  WATERFALL_UPDATED: "Timeline:WaterfallUpdated"
+};
+
+/**
+ * The current target and the timeline front, set by this tool's host.
+ */
+let gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the timeline controller and views.
+ */
+let startupTimeline = Task.async(function*() {
+  yield TimelineView.initialize();
+  yield TimelineController.initialize();
+});
+
+/**
+ * Destroys the timeline controller and views.
+ */
+let shutdownTimeline = Task.async(function*() {
+  yield TimelineView.destroy();
+  yield TimelineController.destroy();
+  yield gFront.stop();
+});
+
+/**
+ * Functions handling the timeline frontend controller.
+ */
+let TimelineController = {
+  /**
+   * Permanent storage for the markers streamed by the backend.
+   */
+  _markers: [],
+
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    this._onRecordingTick = this._onRecordingTick.bind(this);
+    this._onMarkers = this._onMarkers.bind(this);
+    gFront.on("markers", this._onMarkers);
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    gFront.off("markers", this._onMarkers);
+  },
+
+  /**
+   * Gets the accumulated markers in this recording.
+   * @return array.
+   */
+  getMarkers: function() {
+    return this._markers;
+  },
+
+  /**
+   * Starts/stops the timeline recording and streaming.
+   */
+  toggleRecording: Task.async(function*() {
+    let isRecording = yield gFront.isRecording();
+    if (isRecording == false) {
+      yield this._startRecording();
+    } else {
+      yield this._stopRecording();
+    }
+  }),
+
+  /**
+   * Starts the recording, updating the UI as needed.
+   */
+  _startRecording: function*() {
+    this._markers = [];
+    this._markers.startTime = performance.now();
+    this._markers.endTime = performance.now();
+    this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
+
+    TimelineView.handleRecordingStarted();
+    yield gFront.start();
+  },
+
+  /**
+   * Stops the recording, updating the UI as needed.
+   */
+  _stopRecording: function*() {
+    clearInterval(this._updateId);
+
+    TimelineView.handleMarkersUpdate(this._markers);
+    TimelineView.handleRecordingEnded();
+    yield gFront.stop();
+  },
+
+  /**
+   * Used in tests. Stops the recording, discarding the accumulated markers and
+   * updating the UI as needed.
+   */
+  _stopRecordingAndDiscardData: function*() {
+    this._markers.length = 0;
+    yield this._stopRecording();
+  },
+
+  /**
+   * Callback handling the "markers" event on the timeline front.
+   *
+   * @param array markers
+   *        A list of new markers collected since the last time this
+   *        function was invoked.
+   */
+  _onMarkers: function(markers) {
+    Array.prototype.push.apply(this._markers, markers);
+  },
+
+  /**
+   * Callback invoked at a fixed interval while recording.
+   * Updates the markers store with the current time and the timeline overview.
+   */
+  _onRecordingTick: function() {
+    this._markers.endTime = performance.now();
+    TimelineView.handleMarkersUpdate(this._markers);
+  }
+};
+
+/**
+ * Functions handling the timeline frontend view.
+ */
+let TimelineView = {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: Task.async(function*() {
+    this.overview = new Overview($("#timeline-overview"));
+    this.waterfall = new Waterfall($("#timeline-waterfall"));
+
+    this._onSelecting = this._onSelecting.bind(this);
+    this._onRefresh = this._onRefresh.bind(this);
+    this.overview.on("selecting", this._onSelecting);
+    this.overview.on("refresh", this._onRefresh);
+
+    yield this.overview.ready();
+    yield this.waterfall.recalculateBounds();
+  }),
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    this.overview.off("selecting", this._onSelecting);
+    this.overview.off("refresh", this._onRefresh);
+    this.overview.destroy();
+  },
+
+  /**
+   * Signals that a recording session has started and triggers the appropriate
+   * changes in the UI.
+   */
+  handleRecordingStarted: function() {
+    $("#record-button").setAttribute("checked", "true");
+    $("#timeline-pane").selectedPanel = $("#recording-notice");
+
+    this.overview.selectionEnabled = false;
+    this.overview.dropSelection();
+    this.overview.setData([]);
+    this.waterfall.clearView();
+
+    window.emit(EVENTS.RECORDING_STARTED);
+  },
+
+  /**
+   * Signals that a recording session has ended and triggers the appropriate
+   * changes in the UI.
+   */
+  handleRecordingEnded: function() {
+    $("#record-button").removeAttribute("checked");
+    $("#timeline-pane").selectedPanel = $("#timeline-waterfall");
+
+    this.overview.selectionEnabled = true;
+
+    let markers = TimelineController.getMarkers();
+    if (markers.length) {
+      let start = markers[0].start * this.overview.dataScaleX;
+      let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
+      this.overview.setSelection({ start, end });
+    } else {
+      let duration = markers.endTime - markers.startTime;
+      this.waterfall.setData(markers, 0, duration);
+    }
+
+    window.emit(EVENTS.RECORDING_ENDED);
+  },
+
+  /**
+   * Signals that a new set of markers was made available by the controller,
+   * or that the overview graph needs to be updated.
+   *
+   * @param array markers
+   *        A list of new markers collected since the recording has started.
+   */
+  handleMarkersUpdate: function(markers) {
+    this.overview.setData(markers);
+    window.emit(EVENTS.OVERVIEW_UPDATED);
+  },
+
+  /**
+   * Callback handling the "selecting" event on the timeline overview.
+   */
+  _onSelecting: function() {
+    if (!this.overview.hasSelection() &&
+        !this.overview.hasSelectionInProgress()) {
+      this.waterfall.clearView();
+      return;
+    }
+    let selection = this.overview.getSelection();
+    let start = selection.start / this.overview.dataScaleX;
+    let end = selection.end / this.overview.dataScaleX;
+
+    let markers = TimelineController.getMarkers();
+    let timeStart = Math.min(start, end);
+    let timeEnd = Math.max(start, end);
+    this.waterfall.setData(markers, timeStart, timeEnd);
+  },
+
+  /**
+   * Callback handling the "refresh" event on the timeline overview.
+   */
+  _onRefresh: function() {
+    this.waterfall.recalculateBounds();
+    this._onSelecting();
+  }
+};
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * DOM query helpers.
+ */
+function $(selector, target = document) {
+  return target.querySelector(selector);
+}
+function $$(selector, target = document) {
+  return target.querySelectorAll(selector);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/timeline.xul
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/devtools/timeline.css" type="text/css"?>
+<!DOCTYPE window [
+  <!ENTITY % timelineDTD SYSTEM "chrome://browser/locale/devtools/timeline.dtd">
+  %timelineDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script src="chrome://browser/content/devtools/theme-switching.js"/>
+  <script type="application/javascript" src="timeline.js"/>
+
+  <vbox class="theme-body" flex="1">
+    <toolbar id="timeline-toolbar"
+             class="devtools-toolbar">
+      <hbox id="recordings-controls"
+            class="devtools-toolbarbutton-group"
+            align="center">
+        <toolbarbutton id="record-button"
+                       class="devtools-toolbarbutton"
+                       oncommand="TimelineController.toggleRecording()"
+                       tooltiptext="&timelineUI.recordButton.tooltip;"/>
+        <spacer flex="1"/>
+        <label id="record-label"
+               value="&timelineUI.recordLabel;"/>
+      </hbox>
+    </toolbar>
+
+    <vbox id="timeline-overview"/>
+
+    <deck id="timeline-pane"
+          flex="1">
+      <hbox id="empty-notice"
+            class="notice-container"
+            align="center"
+            pack="center"
+            flex="1">
+        <label value="&timelineUI.emptyNotice1;"/>
+        <button id="profiling-notice-button"
+                class="devtools-toolbarbutton"
+                standalone="true"
+                oncommand="TimelineController.toggleRecording()"/>
+        <label value="&timelineUI.emptyNotice2;"/>
+      </hbox>
+
+      <hbox id="recording-notice"
+            class="notice-container"
+            align="center"
+            pack="center"
+            flex="1">
+        <label value="&timelineUI.stopNotice1;"/>
+        <button id="profiling-notice-button"
+                class="devtools-toolbarbutton"
+                standalone="true"
+                checked="true"
+                oncommand="TimelineController.toggleRecording()"/>
+        <label value="&timelineUI.stopNotice2;"/>
+      </hbox>
+
+      <vbox id="timeline-waterfall" flex="1"/>
+    </deck>
+  </vbox>
+</window>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/widgets/global.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+/**
+ * Localization convenience methods.
+ */
+const STRINGS_URI = "chrome://browser/locale/devtools/timeline.properties";
+const L10N = new ViewHelpers.L10N(STRINGS_URI);
+
+/**
+ * A simple schema for mapping markers to the timeline UI. The keys correspond
+ * to marker names, while the values are objects with the following format:
+ *   - group: the row index in the timeline overview graph; multiple markers
+ *            can be added on the same row. @see <overview.js/buildGraphImage>
+ *   - fill: a fill color used when drawing the marker
+ *   - stroke: a stroke color used when drawing the marker
+ *   - label: the label used in the waterfall to identify the marker
+ *
+ * Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
+ * updated as well.
+ */
+const TIMELINE_BLUEPRINT = {
+  "Styles": {
+    group: 0,
+    fill: "hsl(285,50%,68%)",
+    stroke: "hsl(285,50%,48%)",
+    label: L10N.getStr("timeline.label.styles")
+  },
+  "Reflow": {
+    group: 2,
+    fill: "hsl(104,57%,71%)",
+    stroke: "hsl(104,57%,51%)",
+    label: L10N.getStr("timeline.label.reflow")
+  },
+  "Paint": {
+    group: 1,
+    fill: "hsl(39,82%,69%)",
+    stroke: "hsl(39,82%,49%)",
+    label: L10N.getStr("timeline.label.paint")
+  }
+};
+
+// Exported symbols.
+exports.L10N = L10N;
+exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/widgets/overview.js
@@ -0,0 +1,208 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * This file contains the "overview" graph, which is a minimap of all the
+ * timeline data. Regions inside it may be selected, determining which markers
+ * are visible in the "waterfall".
+ */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+Cu.import("resource:///modules/devtools/Graphs.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+loader.lazyRequireGetter(this, "L10N",
+  "devtools/timeline/global", true);
+loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+  "devtools/timeline/global", true);
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const OVERVIEW_HEADER_HEIGHT = 20; // px
+const OVERVIEW_BODY_HEIGHT = 50; // px
+
+const OVERVIEW_BACKGROUND_COLOR = "#fff";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
+const OVERVIEW_SELECTION_LINE_COLOR = "#555";
+const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
+const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+
+const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
+const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
+const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
+const OVERVIEW_HEADER_BACKGROUND = "#ebeced";
+const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
+const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
+const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
+const OVERVIEW_HEADER_TEXT_PADDING = 6; // px
+const OVERVIEW_TIMELINE_STROKES = "#aaa";
+const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
+const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
+const OVERVIEW_GROUP_VERTICAL_PADDING = 6; // px
+const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
+
+/**
+ * An overview for the timeline data.
+ *
+ * @param nsIDOMNode parent
+ *        The parent node holding the overview.
+ */
+function Overview(parent, ...args) {
+  AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]);
+  this.once("ready", () => {
+    this.setBlueprint(TIMELINE_BLUEPRINT);
+
+    var preview = [];
+    preview.startTime = 0;
+    preview.endTime = 1000;
+    this.setData(preview);
+  });
+}
+
+Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+  fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT,
+  clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
+  selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
+  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
+  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
+
+  /**
+   * List of names and colors used to paint this overview.
+   * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js
+   */
+  setBlueprint: function(blueprint) {
+    this._paintBatches = new Map();
+    this._lastGroup = 0;
+
+    for (let type in blueprint) {
+      this._paintBatches.set(type, { style: blueprint[type], batch: [] });
+      this._lastGroup = Math.max(this._lastGroup, blueprint[type].group);
+    }
+  },
+
+  /**
+   * Renders the graph's data source.
+   * @see AbstractCanvasGraph.prototype.buildGraphImage
+   */
+  buildGraphImage: function() {
+    let { canvas, ctx } = this._getNamedCanvas("overview-data");
+    let canvasWidth = this._width;
+    let canvasHeight = this._height;
+    let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
+    let availableWidth = canvasWidth - safeBounds;
+
+    // Group markers into separate paint batches. This is necessary to
+    // draw all markers sharing the same style at once.
+
+    for (let marker of this._data) {
+      this._paintBatches.get(marker.name).batch.push(marker);
+    }
+
+    // Calculate each group's height, and the time-based scaling.
+
+    let totalGroups = this._lastGroup + 1;
+    let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio;
+    let groupHeight = OVERVIEW_BODY_HEIGHT * this._pixelRatio / totalGroups;
+    let groupPadding = OVERVIEW_GROUP_VERTICAL_PADDING * this._pixelRatio;
+
+    let totalTime = (this._data.endTime - this._data.startTime) || 0;
+    let dataScale = this.dataScaleX = availableWidth / totalTime;
+
+    // Draw the header and overview background.
+
+    ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
+    ctx.fillRect(0, 0, canvasWidth, headerHeight);
+
+    ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR;
+    ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
+
+    // Draw the alternating odd/even group backgrounds.
+
+    ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
+    ctx.beginPath();
+
+    for (let i = 1; i < totalGroups; i += 2) {
+      let top = headerHeight + i * groupHeight;
+      ctx.rect(0, top, canvasWidth, groupHeight);
+    }
+
+    ctx.fill();
+
+    // Draw the timeline header ticks.
+
+    ctx.textBaseline = "middle";
+    let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
+    let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+    ctx.font = fontSize + "px " + fontFamily;
+    ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
+    ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
+    ctx.beginPath();
+
+    let tickInterval = this._findOptimalTickInterval(dataScale);
+    let headerTextPadding = OVERVIEW_HEADER_TEXT_PADDING * this._pixelRatio;
+
+    for (let x = 0; x < availableWidth; x += tickInterval) {
+      let left = x + headerTextPadding;
+      let time = Math.round(x / dataScale);
+      let label = L10N.getFormatStr("timeline.tick", time);
+      ctx.fillText(label, left, headerHeight / 2 + 1);
+      ctx.moveTo(x, 0);
+      ctx.lineTo(x, canvasHeight);
+    }
+
+    ctx.stroke();
+
+    // Draw the timeline markers.
+
+    for (let [, { style, batch }] of this._paintBatches) {
+      let top = headerHeight + style.group * groupHeight + groupPadding / 2;
+      let height = groupHeight - groupPadding;
+
+      let gradient = ctx.createLinearGradient(0, top, 0, top + height);
+      gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[0], style.stroke);
+      gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[1], style.fill);
+      gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[2], style.fill);
+      gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[3], style.stroke);
+      ctx.fillStyle = gradient;
+      ctx.beginPath();
+
+      for (let { start, end } of batch) {
+        let left = start * dataScale;
+        let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
+        let width = Math.max(duration * dataScale, this._pixelRatio);
+        ctx.rect(left, top, width, height);
+      }
+
+      ctx.fill();
+
+      // Since all the markers in this batch (thus sharing the same style) have
+      // been drawn, empty it. The next time new markers will be available,
+      // they will be sorted and drawn again.
+      batch.length = 0;
+    }
+
+    return canvas;
+  },
+
+  /**
+   * Finds the optimal tick interval between time markers in this overview.
+   */
+  _findOptimalTickInterval: function(dataScale) {
+    let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE;
+    let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio;
+
+    while (true) {
+      let scaledStep = dataScale * timingStep;
+      if (scaledStep < spacingMin) {
+        timingStep <<= 1;
+        continue;
+      }
+      return scaledStep;
+    }
+  }
+});
+
+exports.Overview = Overview;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/widgets/waterfall.js
@@ -0,0 +1,444 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * This file contains the "waterfall" view, essentially a detailed list
+ * of all the markers in the timeline data.
+ */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+
+loader.lazyRequireGetter(this, "L10N",
+  "devtools/timeline/global", true);
+loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
+  "devtools/timeline/global", true);
+
+loader.lazyImporter(this, "setNamedTimeout",
+  "resource:///modules/devtools/ViewHelpers.jsm");
+loader.lazyImporter(this, "clearNamedTimeout",
+  "resource:///modules/devtools/ViewHelpers.jsm");
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
+const TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
+
+const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms
+const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px
+const TIMELINE_HEADER_TEXT_PADDING = 3; // px
+
+const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px
+const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px
+
+const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
+const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
+const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
+
+/**
+ * A detailed waterfall view for the timeline data.
+ *
+ * @param nsIDOMNode parent
+ *        The parent node holding the waterfall.
+ */
+function Waterfall(parent) {
+  this._parent = parent;
+  this._document = parent.ownerDocument;
+  this._fragment = this._document.createDocumentFragment();
+  this._outstandingMarkers = [];
+
+  this._headerContents = this._document.createElement("hbox");
+  this._headerContents.className = "timeline-header-contents";
+  this._parent.appendChild(this._headerContents);
+
+  this._listContents = this._document.createElement("vbox");
+  this._listContents.className = "timeline-list-contents";
+  this._listContents.setAttribute("flex", "1");
+  this._parent.appendChild(this._listContents);
+
+  this._isRTL = this._getRTL();
+
+  // Lazy require is a bit slow, and these are hot objects.
+  this._l10n = L10N;
+  this._blueprint = TIMELINE_BLUEPRINT;
+  this._setNamedTimeout = setNamedTimeout;
+  this._clearNamedTimeout = clearNamedTimeout;
+}
+
+Waterfall.prototype = {
+  /**
+   * Populates this view with the provided data source.
+   *
+   * @param array markers
+   *        A list of markers received from the controller.
+   * @param number timeStart
+   *        The delta time (in milliseconds) to start drawing from.
+   * @param number timeEnd
+   *        The delta time (in milliseconds) to end drawing at.
+   */
+  setData: function(markers, timeStart, timeEnd) {
+    this.clearView();
+
+    let dataScale = this._waterfallWidth / (timeEnd - timeStart);
+    this._drawWaterfallBackground(dataScale);
+    this._buildHeader(this._headerContents, timeStart, dataScale);
+    this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
+  },
+
+  /**
+   * Depopulates this view.
+   */
+  clearView: function() {
+    while (this._headerContents.hasChildNodes()) {
+      this._headerContents.firstChild.remove();
+    }
+    while (this._listContents.hasChildNodes()) {
+      this._listContents.firstChild.remove();
+    }
+    this._listContents.scrollTop = 0;
+    this._outstandingMarkers.length = 0;
+    this._clearNamedTimeout("flush-outstanding-markers");
+  },
+
+  /**
+   * Calculates and stores the available width for the waterfall.
+   * This should be invoked every time the container window is resized.
+   */
+  recalculateBounds: function() {
+    let bounds = this._parent.getBoundingClientRect();
+    this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH;
+  },
+
+  /**
+   * Creates the header part of this view.
+   *
+   * @param nsIDOMNode parent
+   *        The parent node holding the header.
+   * @param number timeStart
+   *        @see Waterfall.prototype.setData
+   * @param number dataScale
+   *        The time scale of the data source.
+   */
+  _buildHeader: function(parent, timeStart, dataScale) {
+    let container = this._document.createElement("hbox");
+    container.className = "timeline-header-container";
+    container.setAttribute("flex", "1");
+
+    let sidebar = this._document.createElement("hbox");
+    sidebar.className = "timeline-header-sidebar theme-sidebar";
+    sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebar.setAttribute("align", "center");
+    container.appendChild(sidebar);
+
+    let name = this._document.createElement("label");
+    name.className = "plain timeline-header-name";
+    name.setAttribute("value", this._l10n.getStr("timeline.records"));
+    sidebar.appendChild(name);
+
+    let ticks = this._document.createElement("hbox");
+    ticks.className = "timeline-header-ticks";
+    ticks.setAttribute("align", "center");
+    ticks.setAttribute("flex", "1");
+    container.appendChild(ticks);
+
+    let offset = this._isRTL ? this._waterfallWidth : 0;
+    let direction = this._isRTL ? -1 : 1;
+    let tickInterval = this._findOptimalTickInterval({
+      ticksMultiple: TIMELINE_HEADER_TICKS_MULTIPLE,
+      ticksSpacingMin: TIMELINE_HEADER_TICKS_SPACING_MIN,
+      dataScale: dataScale
+    });
+
+    for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
+      let start = x + direction * TIMELINE_HEADER_TEXT_PADDING;
+      let time = Math.round(timeStart + x / dataScale);
+      let label = this._l10n.getFormatStr("timeline.tick", time);
+
+      let node = this._document.createElement("label");
+      node.className = "plain timeline-header-tick";
+      node.style.transform = "translateX(" + (start - offset) + "px)";
+      node.setAttribute("value", label);
+      ticks.appendChild(node);
+    }
+
+    parent.appendChild(container);
+  },
+
+  /**
+   * Creates the markers part of this view.
+   *
+   * @param nsIDOMNode parent
+   *        The parent node holding the markers.
+   * @param number timeStart
+   *        @see Waterfall.prototype.setData
+   * @param number dataScale
+   *        The time scale of the data source.
+   */
+  _buildMarkers: function(parent, markers, timeStart, timeEnd, dataScale) {
+    let processed = 0;
+
+    for (let marker of markers) {
+      if (!isMarkerInRange(marker, timeStart, timeEnd)) {
+        continue;
+      }
+      // Only build and display a finite number of markers initially, to
+      // preserve a snappy UI. After a certain delay, continue building the
+      // outstanding markers while there's (hopefully) no user interaction.
+      let arguments_ = [this._fragment, marker, timeStart, dataScale];
+      if (processed++ < TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT) {
+        this._buildMarker.apply(this, arguments_);
+      } else {
+        this._outstandingMarkers.push(arguments_);
+      }
+    }
+
+    // If there are no outstanding markers, add a dummy "spacer" at the end
+    // to fill up any remaining available space in the UI.
+    if (!this._outstandingMarkers.length) {
+      this._buildMarker(this._fragment, null);
+    }
+    // Otherwise prepare flushing the outstanding markers after a small delay.
+    else {
+      this._setNamedTimeout("flush-outstanding-markers",
+        TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY,
+        () => this._buildOutstandingMarkers(parent));
+    }
+
+    parent.appendChild(this._fragment);
+  },
+
+  /**
+   * Finishes building the outstanding markers in this view.
+   * @see Waterfall.prototype._buildMarkers
+   */
+  _buildOutstandingMarkers: function(parent) {
+    if (!this._outstandingMarkers.length) {
+      return;
+    }
+    for (let args of this._outstandingMarkers) {
+      this._buildMarker.apply(this, args);
+    }
+    this._outstandingMarkers.length = 0;
+    parent.appendChild(this._fragment);
+  },
+
+  /**
+   * Creates a single marker in this view.
+   *
+   * @param nsIDOMNode parent
+   *        The parent node holding the marker.
+   * @param object marker
+   *        The { name, start, end } marker in the data source.
+   * @param timeStart
+   *        @see Waterfall.prototype.setData
+   * @param number dataScale
+   *        @see Waterfall.prototype._buildMarkers
+   */
+  _buildMarker: function(parent, marker, timeStart, dataScale) {
+    let container = this._document.createElement("hbox");
+    container.className = "timeline-marker-container";
+
+    if (marker) {
+      this._buildMarkerSidebar(container, marker);
+      this._buildMarkerWaterfall(container, marker, timeStart, dataScale);
+    } else {
+      this._buildMarkerSpacer(container);
+      container.setAttribute("flex", "1");
+      container.setAttribute("is-spacer", "");
+    }
+
+    parent.appendChild(container);
+  },
+
+  /**
+   * Creates the sidebar part of a marker in this view.
+   *
+   * @param nsIDOMNode container
+   *        The container node representing the marker in this view.
+   * @param object marker
+   *        @see Waterfall.prototype._buildMarker
+   */
+  _buildMarkerSidebar: function(container, marker) {
+    let blueprint = this._blueprint[marker.name];
+
+    let sidebar = this._document.createElement("hbox");
+    sidebar.className = "timeline-marker-sidebar theme-sidebar";
+    sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+    sidebar.setAttribute("align", "center");
+
+    let bullet = this._document.createElement("hbox");
+    bullet.className = "timeline-marker-bullet";
+    bullet.style.backgroundColor = blueprint.fill;
+    bullet.style.borderColor = blueprint.stroke;
+    bullet.setAttribute("type", marker.name);
+    sidebar.appendChild(bullet);
+
+    let name = this._document.createElement("label");
+    name.className = "plain timeline-marker-name";
+    name.setAttribute("value", blueprint.label);
+    sidebar.appendChild(name);
+
+    container.appendChild(sidebar);
+  },
+
+  /**
+   * Creates the waterfall part of a marker in this view.
+   *
+   * @param nsIDOMNode container
+   *        The container node representing the marker.
+   * @param object marker
+   *        @see Waterfall.prototype._buildMarker
+   * @param timeStart
+   *        @see Waterfall.prototype.setData
+   * @param number dataScale
+   *        @see Waterfall.prototype._buildMarkers
+   */
+  _buildMarkerWaterfall: function(container, marker, timeStart, dataScale) {
+    let blueprint = this._blueprint[marker.name];
+
+    let waterfall = this._document.createElement("hbox");
+    waterfall.className = "timeline-marker-waterfall";
+    waterfall.setAttribute("flex", "1");
+
+    let start = (marker.start - timeStart) * dataScale;
+    let width = (marker.end - marker.start) * dataScale;
+    let offset = this._isRTL ? this._waterfallWidth : 0;
+
+    let bar = this._document.createElement("hbox");
+    bar.className = "timeline-marker-bar";
+    bar.style.backgroundColor = blueprint.fill;
+    bar.style.borderColor = blueprint.stroke;
+    bar.style.transform = "translateX(" + (start - offset) + "px)";
+    bar.setAttribute("type", marker.name);
+    bar.setAttribute("width", Math.max(width, TIMELINE_MARKER_BAR_WIDTH_MIN));
+    waterfall.appendChild(bar);
+
+    container.appendChild(waterfall);
+  },
+
+  /**
+   * Creates a dummy spacer as an empty marker.
+   *
+   * @param nsIDOMNode container
+   *        The container node representing the marker.
+   */
+  _buildMarkerSpacer: function(container) {
+    let sidebarSpacer = this._document.createElement("spacer");
+    sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar";
+    sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
+
+    let waterfallSpacer = this._document.createElement("spacer");
+    waterfallSpacer.className = "timeline-marker-waterfall";
+    waterfallSpacer.setAttribute("flex", "1");
+
+    container.appendChild(sidebarSpacer);
+    container.appendChild(waterfallSpacer);
+  },
+
+  /**
+   * Creates the background displayed on the marker's waterfall.
+   *
+   * @param number dataScale
+   *        @see Waterfall.prototype._buildMarkers
+   */
+  _drawWaterfallBackground: function(dataScale) {
+    if (!this._canvas || !this._ctx) {
+      this._canvas = this._document.createElementNS(HTML_NS, "canvas");
+      this._ctx = this._canvas.getContext("2d");
+    }
+    let canvas = this._canvas;
+    let ctx = this._ctx;
+
+    // Nuke the context.
+    let canvasWidth = canvas.width = this._waterfallWidth;
+    let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis.
+
+    // Start over.
+    let imageData = ctx.createImageData(canvasWidth, canvasHeight);
+    let pixelArray = imageData.data;
+
+    let buf = new ArrayBuffer(pixelArray.length);
+    let view8bit = new Uint8ClampedArray(buf);
+    let view32bit = new Uint32Array(buf);
+
+    // Build new millisecond tick lines...
+    let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+    let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+    let tickInterval = this._findOptimalTickInterval({
+      ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
+      ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
+      dataScale: dataScale
+    });
+
+    // Insert one pixel for each division on each scale.
+    for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+      let increment = tickInterval * Math.pow(2, i);
+      for (let x = 0; x < canvasWidth; x += increment) {
+        let position = x | 0;
+        view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+      }
+      alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+    }
+
+    // Flush the image data and cache the waterfall background.
+    pixelArray.set(view8bit);
+    ctx.putImageData(imageData, 0, 0);
+    this._document.mozSetImageElement("waterfall-background", canvas);
+  },
+
+  /**
+   * Finds the optimal tick interval between time markers in this timeline.
+   *
+   * @param number ticksMultiple
+   * @param number ticksSpacingMin
+   * @param number dataScale
+   * @return number
+   */
+  _findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) {
+    let timingStep = ticksMultiple;
+
+    while (true) {
+      let scaledStep = dataScale * timingStep;
+      if (scaledStep < ticksSpacingMin) {
+        timingStep <<= 1;
+        continue;
+      }
+      return scaledStep;
+    }
+  },
+
+  /**
+   * Returns true if this is document is in RTL mode.
+   * @return boolean
+   */
+  _getRTL: function() {
+    let win = this._document.defaultView;
+    let doc = this._document.documentElement;
+    return win.getComputedStyle(doc, null).direction == "rtl";
+  }
+};
+
+/**
+ * Checks if a given marker is in the specified time range.
+ *
+ * @param object e
+ *        The marker containing the { start, end } timestamps.
+ * @param number start
+ *        The earliest allowed time.
+ * @param number end
+ *        The latest allowed time.
+ * @return boolean
+ *         True if the marker fits inside the specified time range.
+ */
+function isMarkerInRange(e, start, end) {
+  return (e.start >= start && e.end <= end) || // bounds inside
+         (e.start < start && e.end > end) || // bounds outside
+         (e.start < start && e.end >= start && e.end <= end) || // overlap start
+         (e.end > end && e.start >= start && e.start <= end); // overlap end
+}
+
+exports.Waterfall = Waterfall;
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.dtd
@@ -0,0 +1,30 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the Timeline strings -->
+<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
+
+<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
+  - keep it in English, or another language commonly spoken among web developers.
+  - You want to make that choice consistent across the developer tools.
+  - A good criteria is the language in which you'd find the best
+  - documentation on web development on the web. -->
+
+<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
+  -  on a button that starts a new recording. -->
+<!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
+
+<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
+  -  as a label to signal that a recording is in progress. -->
+<!ENTITY timelineUI.recordLabel "Recording…">
+
+<!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
+  -  in the timeline view when empty. -->
+<!ENTITY timelineUI.emptyNotice1    "Click on the">
+<!ENTITY timelineUI.emptyNotice2    "button to start recording timeline events.">
+
+<!-- LOCALIZATION NOTE (timelineUI.stopNotice1/2): This is the label shown
+  -  in the timeline view while recording. -->
+<!ENTITY timelineUI.stopNotice1    "Click on the">
+<!ENTITY timelineUI.stopNotice2    "button again to stop recording.">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties
@@ -0,0 +1,40 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Timeline
+# which is available from the Web Developer sub-menu -> 'Timeline'.
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (timeline.label):
+# This string is displayed in the title of the tab when the timeline is
+# displayed inside the developer tools window and in the Developer Tools Menu.
+timeline.label=Timeline
+
+# LOCALIZATION NOTE (timeline.panelLabel):
+# This is used as the label for the toolbox panel.
+timeline.panelLabel=Timeline Panel
+
+# LOCALIZATION NOTE (timeline.tooltip):
+# This string is displayed in the tooltip of the tab when the timeline is
+# displayed inside the developer tools window.
+timeline.tooltip=Performance Timeline
+
+# LOCALIZATION NOTE (timeline.tick):
+# This string is displayed in the timeline overview, for delimiting ticks
+# by time, in milliseconds.
+timeline.tick=%S ms
+
+# LOCALIZATION NOTE (timeline.records):
+# This string is displayed in the timeline waterfall, as a title for the menu.
+timeline.records=RECORDS
+
+# LOCALIZATION NOTE (timeline.label.*):
+# These strings are displayed in the timeline waterfall, identifying markers.
+timeline.label.styles=Styles
+timeline.label.reflow=Reflow
+timeline.label.paint=Paint
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -53,16 +53,18 @@
     locale/browser/devtools/sourceeditor.dtd          (%chrome/browser/devtools/sourceeditor.dtd)
     locale/browser/devtools/profiler.dtd              (%chrome/browser/devtools/profiler.dtd)
     locale/browser/devtools/profiler.properties       (%chrome/browser/devtools/profiler.properties)
     locale/browser/devtools/layoutview.dtd            (%chrome/browser/devtools/layoutview.dtd)
     locale/browser/devtools/responsiveUI.properties   (%chrome/browser/devtools/responsiveUI.properties)
     locale/browser/devtools/toolbox.dtd            (%chrome/browser/devtools/toolbox.dtd)
     locale/browser/devtools/toolbox.properties     (%chrome/browser/devtools/toolbox.properties)
     locale/browser/devtools/inspector.dtd          (%chrome/browser/devtools/inspector.dtd)
+    locale/browser/devtools/timeline.dtd           (%chrome/browser/devtools/timeline.dtd)
+    locale/browser/devtools/timeline.properties    (%chrome/browser/devtools/timeline.properties)
     locale/browser/devtools/projecteditor.properties     (%chrome/browser/devtools/projecteditor.properties)
     locale/browser/devtools/eyedropper.properties     (%chrome/browser/devtools/eyedropper.properties)
     locale/browser/devtools/connection-screen.dtd  (%chrome/browser/devtools/connection-screen.dtd)
     locale/browser/devtools/connection-screen.properties (%chrome/browser/devtools/connection-screen.properties)
     locale/browser/devtools/font-inspector.dtd     (%chrome/browser/devtools/font-inspector.dtd)
     locale/browser/devtools/app-manager.dtd        (%chrome/browser/devtools/app-manager.dtd)
     locale/browser/devtools/app-manager.properties (%chrome/browser/devtools/app-manager.properties)
     locale/browser/devtools/webide.dtd             (%chrome/browser/devtools/webide.dtd)
new file mode 100644
--- /dev/null
+++ b/browser/themes/linux/devtools/timeline.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%include ../../shared/devtools/timeline.inc.css
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -242,16 +242,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 * skin/classic/browser/devtools/canvasdebugger.css    (devtools/canvasdebugger.css)
 * skin/classic/browser/devtools/debugger.css          (devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css        (../shared/devtools/eyedropper.css)
 * skin/classic/browser/devtools/netmonitor.css        (devtools/netmonitor.css)
 * skin/classic/browser/devtools/profiler.css          (devtools/profiler.css)
+* skin/classic/browser/devtools/timeline.css          (devtools/timeline.css)
 * skin/classic/browser/devtools/scratchpad.css        (devtools/scratchpad.css)
 * skin/classic/browser/devtools/shadereditor.css      (devtools/shadereditor.css)
 * skin/classic/browser/devtools/splitview.css         (../shared/devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css       (../shared/devtools/styleeditor.css)
   skin/classic/browser/devtools/storage.css           (../shared/devtools/storage.css)
 * skin/classic/browser/devtools/webaudioeditor.css    (devtools/webaudioeditor.css)
   skin/classic/browser/devtools/magnifying-glass.png        (../shared/devtools/images/magnifying-glass.png)
   skin/classic/browser/devtools/magnifying-glass@2x.png     (../shared/devtools/images/magnifying-glass@2x.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/osx/devtools/timeline.css
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%include ../shared.inc
+%include ../../shared/devtools/timeline.inc.css
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -369,16 +369,17 @@ browser.jar:
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 * skin/classic/browser/devtools/canvasdebugger.css          (devtools/canvasdebugger.css)
 * skin/classic/browser/devtools/debugger.css                (devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css              (../shared/devtools/eyedropper.css)
 * skin/classic/browser/devtools/netmonitor.css              (devtools/netmonitor.css)
 * skin/classic/browser/devtools/profiler.css                (devtools/profiler.css)
+* skin/classic/browser/devtools/timeline.css                (devtools/timeline.css)
 * skin/classic/browser/devtools/scratchpad.css              (devtools/scratchpad.css)
 * skin/classic/browser/devtools/shadereditor.css            (devtools/shadereditor.css)
 * skin/classic/browser/devtools/splitview.css               (../shared/devtools/splitview.css)
   skin/classic/browser/devtools/styleeditor.css             (../shared/devtools/styleeditor.css)
   skin/classic/browser/devtools/storage.css                 (../shared/devtools/storage.css)
 * skin/classic/browser/devtools/webaudioeditor.css          (devtools/webaudioeditor.css)
   skin/classic/browser/devtools/magnifying-glass.png        (../shared/devtools/images/magnifying-glass.png)
   skin/classic/browser/devtools/magnifying-glass@2x.png     (../shared/devtools/images/magnifying-glass@2x.png)
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -886,17 +886,23 @@ toolbarbutton[panel-multiview-anchor="tr
   position: absolute;
   top: 0;
   height: 100%;
   width: @exitSubviewGutterWidth@;
   background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted.png),
                     linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0));
   background-repeat: no-repeat;
   background-color: Highlight;
-  background-position: left 10px center, 0; /* this doesn't need to be changed for RTL */
+  background-position: left 10px center, 0;
+}
+
+#PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after {
+  background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl.png),
+                    linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0));
+  background-position: right 10px center, 0;
 }
 
 toolbarbutton[panel-multiview-anchor="true"] {
   background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted.png),
                     linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0));
   background-position: right calc(@menuPanelButtonWidth@ / 2 - @exitSubviewGutterWidth@ + 2px) center;
   background-repeat: no-repeat, repeat;
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/timeline.inc.css
@@ -0,0 +1,159 @@
+/* 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/. */
+
+#record-button {
+  list-style-image: url(profiler-stopwatch.svg);
+}
+
+#record-button[checked] {
+  list-style-image: url(profiler-stopwatch-checked.svg);
+}
+
+#record-button:not([checked]) ~ #record-label {
+  visibility: hidden;
+}
+
+.notice-container {
+  font-size: 120%;
+  padding-bottom: 35vh;
+}
+
+.theme-dark .notice-container {
+  background: #343c45; /* Toolbars */
+  color: #f5f7fa; /* Light foreground text */
+}
+
+.theme-light .notice-container {
+  background: #f0f1f2; /* Toolbars */
+  color: #585959; /* Grey foreground text */
+}
+
+#empty-notice button,
+#recording-notice button {
+  min-width: 30px;
+  min-height: 28px;
+  margin: 0;
+  list-style-image: url(profiler-stopwatch.svg);
+}
+
+#empty-notice button[checked],
+#recording-notice button[checked] {
+  list-style-image: url(profiler-stopwatch-checked.svg);
+}
+
+#empty-notice button .button-text,
+#recording-notice button .button-text {
+  display: none;
+}
+
+.theme-dark #timeline-overview {
+  border-bottom: 1px solid #000;
+}
+
+.theme-light #timeline-overview {
+  border-bottom: 1px solid #aaa;
+}
+
+.timeline-list-contents {
+  /* Hack: force hardware acceleration */
+  transform: translateZ(1px);
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.timeline-header-ticks,
+.timeline-marker-waterfall {
+  /* Background created on a <canvas> in js. */
+  /* @see browser/devtools/timeline/widgets/waterfall.js */
+  background-image: -moz-element(#waterfall-background);
+  background-repeat: repeat-y;
+  background-position: -1px center;
+}
+
+.timeline-marker-waterfall {
+  overflow: hidden;
+}
+
+.timeline-marker-container[is-spacer] {
+  pointer-events: none;
+}
+
+.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) {
+  background-color: rgba(255,255,255,0.03);
+}
+
+.theme-light .timeline-marker-container:not([is-spacer]):nth-child(2n) {
+  background-color: rgba(128,128,128,0.03);
+}
+
+.theme-dark .timeline-marker-container:hover {
+  background-color: rgba(255,255,255,0.1) !important;
+}
+
+.theme-light .timeline-marker-container:hover {
+  background-color: rgba(128,128,128,0.1) !important;
+}
+
+.timeline-header-sidebar,
+.timeline-marker-sidebar {
+  -moz-border-end: 1px solid;
+}
+
+.theme-dark .timeline-header-sidebar,
+.theme-dark .timeline-marker-sidebar {
+  -moz-border-end-color: #000;
+}
+
+.theme-light .timeline-header-sidebar,
+.theme-light .timeline-marker-sidebar {
+  -moz-border-end-color: #aaa;
+}
+
+.timeline-header-sidebar {
+  padding: 5px;
+}
+
+.timeline-marker-sidebar {
+  padding: 2px;
+}
+
+.timeline-marker-container:hover > .timeline-marker-sidebar {
+  background-color: transparent;
+}
+
+.timeline-header-tick {
+  width: 100px;
+  font-size: 9px;
+  transform-origin: left center;
+}
+
+.theme-dark .timeline-header-tick {
+  color: #a9bacb;
+}
+
+.theme-light .timeline-header-tick {
+  color: #292e33;
+}
+
+.timeline-header-tick:not(:first-child) {
+  -moz-margin-start: -100px !important; /* Don't affect layout. */
+}
+
+.timeline-marker-bullet {
+  width: 8px;
+  height: 8px;
+  -moz-margin-start: 8px;
+  -moz-margin-end: 6px;
+  border: 1px solid;
+  border-radius: 1px;
+}
+
+.timeline-marker-bar {
+  margin-top: 4px;
+  margin-bottom: 4px;
+  border: 1px solid;
+  border-radius: 1px;
+  transform-origin: left center;
+}
new file mode 100644
--- /dev/null
+++ b/browser/themes/windows/devtools/timeline.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%include ../../shared/devtools/timeline.inc.css
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -279,16 +279,17 @@ browser.jar:
         skin/classic/browser/devtools/breadcrumbs-divider@2x.png    (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton.png  (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
         skin/classic/browser/devtools/eyedropper.css                (../shared/devtools/eyedropper.css)
 *       skin/classic/browser/devtools/canvasdebugger.css            (devtools/canvasdebugger.css)
 *       skin/classic/browser/devtools/debugger.css                  (devtools/debugger.css)
 *       skin/classic/browser/devtools/netmonitor.css                (devtools/netmonitor.css)
 *       skin/classic/browser/devtools/profiler.css                  (devtools/profiler.css)
+*       skin/classic/browser/devtools/timeline.css                  (devtools/timeline.css)
 *       skin/classic/browser/devtools/scratchpad.css                (devtools/scratchpad.css)
 *       skin/classic/browser/devtools/shadereditor.css              (devtools/shadereditor.css)
         skin/classic/browser/devtools/storage.css                   (../shared/devtools/storage.css)
 *       skin/classic/browser/devtools/splitview.css                 (../shared/devtools/splitview.css)
         skin/classic/browser/devtools/styleeditor.css               (../shared/devtools/styleeditor.css)
 *       skin/classic/browser/devtools/webaudioeditor.css            (devtools/webaudioeditor.css)
         skin/classic/browser/devtools/magnifying-glass.png          (../shared/devtools/images/magnifying-glass.png)
         skin/classic/browser/devtools/magnifying-glass@2x.png       (../shared/devtools/images/magnifying-glass@2x.png)
@@ -699,16 +700,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
 *       skin/classic/aero/browser/devtools/canvasdebugger.css        (devtools/canvasdebugger.css)
 *       skin/classic/aero/browser/devtools/debugger.css              (devtools/debugger.css)
         skin/classic/aero/browser/devtools/eyedropper.css            (../shared/devtools/eyedropper.css)
 *       skin/classic/aero/browser/devtools/netmonitor.css            (devtools/netmonitor.css)
 *       skin/classic/aero/browser/devtools/profiler.css              (devtools/profiler.css)
+*       skin/classic/aero/browser/devtools/timeline.css              (devtools/timeline.css)
 *       skin/classic/aero/browser/devtools/scratchpad.css            (devtools/scratchpad.css)
 *       skin/classic/aero/browser/devtools/shadereditor.css          (devtools/shadereditor.css)
 *       skin/classic/aero/browser/devtools/splitview.css             (../shared/devtools/splitview.css)
         skin/classic/aero/browser/devtools/styleeditor.css           (../shared/devtools/styleeditor.css)
         skin/classic/aero/browser/devtools/storage.css               (../shared/devtools/storage.css)
 *       skin/classic/aero/browser/devtools/webaudioeditor.css        (devtools/webaudioeditor.css)
         skin/classic/aero/browser/devtools/magnifying-glass.png      (../shared/devtools/images/magnifying-glass.png)
         skin/classic/aero/browser/devtools/magnifying-glass@2x.png   (../shared/devtools/images/magnifying-glass@2x.png)
--- a/dom/camera/GonkCameraControl.cpp
+++ b/dom/camera/GonkCameraControl.cpp
@@ -785,62 +785,44 @@ nsGonkCameraControl::SetPictureSizeImpl(
     return NS_ERROR_INVALID_ARG;
   }
 
   if (aSize.width == mLastPictureSize.width && aSize.height == mLastPictureSize.height) {
     DOM_CAMERA_LOGI("Requested picture size %ux%u unchanged\n", aSize.width, aSize.height);
     return NS_OK;
   }
 
-  /**
-   * Choose the supported picture size that is closest in area to the
-   * specified size. Some drivers will fail to take a picture if the
-   * thumbnail size is not the same aspect ratio, so we update that
-   * as well to a size closest to the last user-requested one.
-   */
-  int smallestDelta = INT_MAX;
-  uint32_t smallestDeltaIndex = UINT32_MAX;
-  int targetArea = aSize.width * aSize.height;
-
   nsAutoTArray<Size, 8> supportedSizes;
   Get(CAMERA_PARAM_SUPPORTED_PICTURESIZES, supportedSizes);
 
-  for (uint32_t i = 0; i < supportedSizes.Length(); ++i) {
-    int area = supportedSizes[i].width * supportedSizes[i].height;
-    int delta = abs(area - targetArea);
-
-    if (area != 0 && delta < smallestDelta) {
-      smallestDelta = delta;
-      smallestDeltaIndex = i;
-    }
-  }
-
-  if (smallestDeltaIndex == UINT32_MAX) {
+  Size best;
+  nsresult rv = GetSupportedSize(aSize, supportedSizes, best);
+  if (NS_FAILED(rv)) {
     DOM_CAMERA_LOGW("Unable to find a picture size close to %ux%u\n",
       aSize.width, aSize.height);
     return NS_ERROR_INVALID_ARG;
   }
 
-  Size size = supportedSizes[smallestDeltaIndex];
   DOM_CAMERA_LOGI("camera-param set picture-size = %ux%u (requested %ux%u)\n",
-    size.width, size.height, aSize.width, aSize.height);
-  if (size.width > INT32_MAX || size.height > INT32_MAX) {
+    best.width, best.height, aSize.width, aSize.height);
+  if (best.width > INT32_MAX || best.height > INT32_MAX) {
     DOM_CAMERA_LOGE("Supported picture size is too big, no change\n");
     return NS_ERROR_FAILURE;
   }
 
-  nsresult rv = mParams.Set(CAMERA_PARAM_PICTURE_SIZE, size);
+  rv = mParams.Set(CAMERA_PARAM_PICTURE_SIZE, best);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
-  mLastPictureSize = size;
+  mLastPictureSize = best;
 
-  // Finally, update the thumbnail size in case the picture
-  // aspect ratio changed.
+  // Finally, update the thumbnail size in case the picture aspect ratio changed.
+  // Some drivers will fail to take a picture if the thumbnail size is not the
+  // same aspect ratio as the picture size.
   return UpdateThumbnailSize();
 }
 
 int32_t
 nsGonkCameraControl::RationalizeRotation(int32_t aRotation)
 {
   int32_t r = aRotation;
 
@@ -1278,17 +1260,17 @@ nsGonkCameraControl::SetPreviewSize(cons
   nsTArray<Size> previewSizes;
   nsresult rv = Get(CAMERA_PARAM_SUPPORTED_PREVIEWSIZES, previewSizes);
   if (NS_FAILED(rv)) {
     DOM_CAMERA_LOGE("Camera failed to return any preview sizes (0x%x)\n", rv);
     return rv;
   }
 
   Size best;
-  rv  = GetSupportedSize(aSize, previewSizes, best);
+  rv = GetSupportedSize(aSize, previewSizes, best);
   if (NS_FAILED(rv)) {
     DOM_CAMERA_LOGE("Failed to find a supported preview size, requested size %dx%d",
         aSize.width, aSize.height);
     return rv;
   }
 
   // Some camera drivers will ignore our preview size if it's larger
   // than the currently set video recording size, so we need to set
@@ -1331,19 +1313,29 @@ nsGonkCameraControl::GetSupportedSize(co
   nsresult rv = NS_ERROR_INVALID_ARG;
   best = aSize;
   uint32_t minSizeDelta = UINT32_MAX;
   uint32_t delta;
 
   if (!aSize.width && !aSize.height) {
     // no size specified, take the first supported size
     best = supportedSizes[0];
-    rv = NS_OK;
+    return NS_OK;
   } else if (aSize.width && aSize.height) {
-    // both height and width specified, find the supported size closest to requested size
+    // both height and width specified, find the supported size closest to
+    // the requested size, looking for an exact match first
+    for (nsTArray<Size>::index_type i = 0; i < supportedSizes.Length(); i++) {
+      Size size = supportedSizes[i];
+      if (size.width == aSize.width && size.height == aSize.height) {
+        best = size;
+        return NS_OK;
+      }
+    }
+
+    // no exact matches--look for a match closest in area
     uint32_t targetArea = aSize.width * aSize.height;
     for (nsTArray<Size>::index_type i = 0; i < supportedSizes.Length(); i++) {
       Size size = supportedSizes[i];
       uint32_t delta = abs((long int)(size.width * size.height - targetArea));
       if (delta < minSizeDelta) {
         minSizeDelta = delta;
         best = size;
         rv = NS_OK;
--- a/dom/camera/test/test_camera_fake_parameters.html
+++ b/dom/camera/test/test_camera_fake_parameters.html
@@ -331,20 +331,85 @@ var tests = [
          "exposureCompensation(-2^32) = " + cam.exposureCompensation);
       cam.exposureCompensation = Math.pow(2, 32);
       ok(cam.exposureCompensation == cap.maxExposureCompensation,
          "exposureCompensation(2^32) = " + cam.exposureCompensation);
 
       next();
     },
   },
+  {
+    key: "bug-1054803",
+    prep: function setupFakePictureSizes(test) {
+      // The important part of this test is that 3264 * 1836 = 5,992,704 = 2448 * 2448,
+      // so we need to make sure that the size-matching algorithm picks the right size.
+      test.setFakeParameters("picture-size-values=3264x1836,2448x2448,1836x3264", function () {
+        run();
+      });
+    },
+    test: function testFakeFocusAreas(cam, cap) {
+      // validate the capability attribute
+      ok(cap.pictureSizes.length == 3, "pictureSizes.length = " + cap.pictureSizes.length);
+      var found = 0;
+      [ { height: 3264, width: 1836 },
+        { height: 1836, width: 3264 },
+        { height: 2448, width: 2448 } ].forEach(function(size) {
+        found = 0;
+        cap.pictureSizes.forEach(function(capSize) {
+          if (capSize.height == size.height && capSize.width == size.width) {
+            ++found;
+          }
+        });
+        ok(found == 1, "found size " + size.toSource() + " in pictureSizes");
+      });
+
+      // test setters and getters
+      var sync = new Promise(function(resolve, reject) {
+        // Use setConfiguration() (which will fail on the profile)
+        // to signify that the CameraControl thread has run and our
+        // settings are applied. Yes--this is an ugly hack.
+        cam.setConfiguration({ mode: 'video',
+                               recorderProfile: 'weird-unsupported-profile'
+                             }, resolve, resolve);
+      });
+      var sizeGenerator = function() {
+        var sizes = [ { height: 3264, width: 1836 },
+                      { height: 1836, width: 3264 },
+                      { height: 2448, width: 2448 } ];
+        for (var i = 0; i < sizes.length; ++i) {
+          yield sizes[i];
+        }
+      }();
+
+      function nextSize() {
+        try {
+          var size = sizeGenerator.next();
+          cam.setPictureSize(size);
+          sync.then(function() {
+            var got = cam.getPictureSize();
+            ok(got.width == size.width && got.height == size.height,
+              "Set size " + size.toSource() + ", got size " + got.toSource());
+            nextSize();
+          }, onError);
+        } catch(e) {
+          if (e instanceof StopIteration) {
+            next();
+          } else {
+            throw e;
+          }
+        }
+      }
+
+      nextSize();
+    },
+  },
 ];
 
 var testGenerator = function() {
-  for (var i = 0; i < tests.length; ++i ) {
+  for (var i = 0; i < tests.length; ++i) {
     yield tests[i];
   }
 }();
 
 window.addEventListener('beforeunload', function() {
   document.getElementById('viewfinder').mozSrcObject = null;
   if (cameraObj) {
     cameraObj.release();
--- a/dom/nfc/MozNDEFRecord.cpp
+++ b/dom/nfc/MozNDEFRecord.cpp
@@ -59,28 +59,68 @@ MozNDEFRecord::DropData()
     mId = nullptr;
   }
   if (mPayload) {
     mPayload = nullptr;
   }
   mozilla::DropJSObjects(this);
 }
 
+/**
+ * Validate TNF.
+ * See section 3.3 THE NDEF Specification Test Requirements,
+ * NDEF specification 1.0
+ */
+/* static */
+bool
+MozNDEFRecord::ValidateTNF(const MozNDEFRecordOptions& aOptions,
+                           ErrorResult& aRv)
+{
+  // * The TNF field MUST have a value between 0x00 and 0x06.
+  // * The TNF value MUST NOT be 0x07.
+  // These two requirements are already handled by WebIDL bindings.
+
+  // If the TNF value is 0x00 (Empty), the TYPE, ID, and PAYLOAD fields MUST be
+  // omitted from the record.
+  if ((aOptions.mTnf == TNF::Empty) &&
+      (aOptions.mType.WasPassed() || aOptions.mId.WasPassed() ||
+       aOptions.mPayload.WasPassed())) {
+    NS_WARNING("tnf is empty but type/id/payload is not null.");
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return false;
+  }
+
+  // If the TNF value is 0x05 (Unknown) or 0x06(Unchanged), the TYPE field MUST
+  // be omitted from the NDEF record.
+  if ((aOptions.mTnf == TNF::Unknown || aOptions.mTnf == TNF::Unchanged) &&
+      aOptions.mType.WasPassed()) {
+    NS_WARNING("tnf is unknown/unchanged but type  is not null.");
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return false;
+  }
+
+  return true;
+}
+
 /* static */
 already_AddRefed<MozNDEFRecord>
 MozNDEFRecord::Constructor(const GlobalObject& aGlobal,
                            const MozNDEFRecordOptions& aOptions,
                            ErrorResult& aRv)
 {
   nsCOMPtr<nsPIDOMWindow> win = do_QueryInterface(aGlobal.GetAsSupports());
   if (!win) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
+  if (!ValidateTNF(aOptions, aRv)) {
+    return nullptr;
+  }
+
   nsRefPtr<MozNDEFRecord> ndefrecord = new MozNDEFRecord(aGlobal.Context(),
                                                          win, aOptions);
   if (!ndefrecord) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
   return ndefrecord.forget();
 }
--- a/dom/nfc/MozNDEFRecord.h
+++ b/dom/nfc/MozNDEFRecord.h
@@ -10,16 +10,17 @@
 #define mozilla_dom_MozNDEFRecord_h__
 
 #include "mozilla/Attributes.h"
 #include "mozilla/ErrorResult.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsWrapperCache.h"
 #include "jsapi.h"
 
+#include "mozilla/dom/MozNDEFRecordBinding.h"
 #include "mozilla/dom/TypedArray.h"
 #include "jsfriendapi.h"
 #include "js/GCAPI.h"
 #include "nsPIDOMWindow.h"
 
 struct JSContext;
 
 namespace mozilla {
@@ -48,17 +49,17 @@ public:
 
   virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
 
   static already_AddRefed<MozNDEFRecord>
   Constructor(const GlobalObject& aGlobal,
               const MozNDEFRecordOptions& aOptions,
               ErrorResult& aRv);
 
-  uint8_t Tnf() const
+  TNF Tnf() const
   {
     return mTnf;
   }
 
   void GetType(JSContext* cx, JS::MutableHandle<JSObject*> retval) const
   {
     if (mType) {
       JS::ExposeObjectToActiveJS(mType);
@@ -83,17 +84,20 @@ public:
   }
 
 private:
   MozNDEFRecord() MOZ_DELETE;
   nsRefPtr<nsPIDOMWindow> mWindow;
   void HoldData();
   void DropData();
 
-  uint8_t mTnf;
+  static bool
+  ValidateTNF(const MozNDEFRecordOptions& aOptions, ErrorResult& aRv);
+
+  TNF mTnf;
   JS::Heap<JSObject*> mType;
   JS::Heap<JSObject*> mId;
   JS::Heap<JSObject*> mPayload;
 };
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/nfc/gonk/NfcMessageHandler.cpp
+++ b/dom/nfc/gonk/NfcMessageHandler.cpp
@@ -1,23 +1,25 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "NfcMessageHandler.h"
 #include <binder/Parcel.h>
+#include "mozilla/dom/MozNDEFRecordBinding.h"
 #include "nsDebug.h"
 #include "NfcGonkMessage.h"
 #include "NfcOptions.h"
 
 #include <android/log.h>
 #define CHROMIUM_LOG(args...)  __android_log_print(ANDROID_LOG_INFO, "NfcMessageHandler", args)
 
 using namespace android;
 using namespace mozilla;
+using namespace mozilla::dom;
 
 static const char* kConfigRequest = "config";
 static const char* kGetDetailsNDEF = "getDetailsNDEF";
 static const char* kReadNDEFRequest = "readNDEF";
 static const char* kWriteNDEFRequest = "writeNDEF";
 static const char* kMakeReadOnlyNDEFRequest = "makeReadOnlyNDEF";
 static const char* kConnectRequest = "connect";
 static const char* kCloseRequest = "close";
@@ -325,17 +327,17 @@ bool
 NfcMessageHandler::ReadNDEFMessage(const Parcel& aParcel, EventOptions& aOptions)
 {
   int32_t recordCount = aParcel.readInt32();
   aOptions.mRecords.SetCapacity(recordCount);
 
   for (int i = 0; i < recordCount; i++) {
     int32_t tnf = aParcel.readInt32();
     NDEFRecordStruct record;
-    record.mTnf = tnf;
+    record.mTnf = static_cast<TNF>(tnf);
 
     int32_t typeLength = aParcel.readInt32();
     record.mType.AppendElements(
        static_cast<const uint8_t*>(aParcel.readInplace(typeLength)), typeLength);
 
     int32_t idLength = aParcel.readInt32();
     record.mId.AppendElements(
        static_cast<const uint8_t*>(aParcel.readInplace(idLength)), idLength);
@@ -352,17 +354,17 @@ NfcMessageHandler::ReadNDEFMessage(const
 
 bool
 NfcMessageHandler::WriteNDEFMessage(Parcel& aParcel, const CommandOptions& aOptions)
 {
   int recordCount = aOptions.mRecords.Length();
   aParcel.writeInt32(recordCount);
   for (int i = 0; i < recordCount; i++) {
     const NDEFRecordStruct& record = aOptions.mRecords[i];
-    aParcel.writeInt32(record.mTnf);
+    aParcel.writeInt32(static_cast<int32_t>(record.mTnf));
 
     void* data;
 
     aParcel.writeInt32(record.mType.Length());
     data = aParcel.writeInplace(record.mType.Length());
     memcpy(data, record.mType.Elements(), record.mType.Length());
 
     aParcel.writeInt32(record.mId.Length());
--- a/dom/nfc/gonk/NfcOptions.h
+++ b/dom/nfc/gonk/NfcOptions.h
@@ -1,22 +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/. */
 
 #ifndef NfcOptions_h
 #define NfcOptions_h
 
 #include "mozilla/dom/NfcOptionsBinding.h"
+#include "mozilla/dom/MozNDEFRecordBinding.h"
 
 namespace mozilla {
 
 struct NDEFRecordStruct
 {
-  uint8_t mTnf;
+  dom::TNF mTnf;
   nsTArray<uint8_t> mType;
   nsTArray<uint8_t> mId;
   nsTArray<uint8_t> mPayload;
 };
 
 struct CommandOptions
 {
   CommandOptions(const mozilla::dom::NfcCommandOptions& aOther) {
--- a/dom/nfc/gonk/NfcService.cpp
+++ b/dom/nfc/gonk/NfcService.cpp
@@ -20,17 +20,17 @@
 #define NS_NFCSERVICE_CONTRACTID "@mozilla.org/nfc/service;1"
 
 using namespace android;
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
 
 static const nsLiteralString SEOriginString[] = {
   NS_LITERAL_STRING("SIM"),
-  NS_LITERAL_STRING("ESE"),
+  NS_LITERAL_STRING("eSE"),
   NS_LITERAL_STRING("ASSD")
 };
 
 namespace mozilla {
 
 static NfcService* gNfcService;
 
 NS_IMPL_ISUPPORTS(NfcService, nsINfcService)
@@ -128,16 +128,17 @@ public:
         return NS_ERROR_FAILURE;
       }
 
       for (int i = 0; i < length; i++) {
         NDEFRecordStruct& recordStruct = mEvent.mRecords[i];
         MozNDEFRecordOptions& record = *event.mRecords.Value().AppendElement();
 
         record.mTnf = recordStruct.mTnf;
+        MOZ_ASSERT(record.mTnf < TNF::EndGuard_);
 
         if (recordStruct.mType.Length() > 0) {
           record.mType.Construct();
           record.mType.Value().Init(Uint8Array::Create(cx, recordStruct.mType.Length(), recordStruct.mType.Elements()));
         }
 
         if (recordStruct.mId.Length() > 0) {
           record.mId.Construct();
--- a/dom/nfc/nsNfc.js
+++ b/dom/nfc/nsNfc.js
@@ -17,18 +17,16 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "appsService",
                                    "@mozilla.org/AppsService;1",
                                    "nsIAppsService");
-const NFC_PEER_EVENT_READY = 0x01;
-const NFC_PEER_EVENT_LOST  = 0x02;
 
 /**
  * NFCTag
  */
 function MozNFCTag() {
   debug("In MozNFCTag Constructor");
   this._nfcContentHelper = Cc["@mozilla.org/nfc/content-helper;1"]
                              .getService(Ci.nsINfcContentHelper);
@@ -221,29 +219,27 @@ mozNfc.prototype = {
   get onpeerlost() {
     return this.__DOM_IMPL__.getEventHandler("onpeerlost");
   },
 
   set onpeerlost(handler) {
     this.__DOM_IMPL__.setEventHandler("onpeerlost", handler);
   },
 
-  eventListenerWasAdded: function(evt) {
-    let eventType = this.getEventType(evt);
-    if (eventType != NFC_PEER_EVENT_READY) {
+  eventListenerWasAdded: function(eventType) {
+    if (eventType !== "peerready") {
       return;
     }
 
     let appId = this._window.document.nodePrincipal.appId;
     this._nfcContentHelper.registerTargetForPeerReady(this._window, appId);
   },
 
-  eventListenerWasRemoved: function(evt) {
-    let eventType = this.getEventType(evt);
-    if (eventType != NFC_PEER_EVENT_READY) {
+  eventListenerWasRemoved: function(eventType) {
+    if (eventType !== "peerready") {
       return;
     }
 
     let appId = this._window.document.nodePrincipal.appId;
     this._nfcContentHelper.unregisterTargetForPeerReady(this._window, appId);
   },
 
   notifyPeerReady: function notifyPeerReady(sessionToken) {
@@ -280,31 +276,16 @@ mozNfc.prototype = {
 
     this.session = null;
 
     debug("fire onpeerlost");
     let event = new this._window.Event("peerlost");
     this.__DOM_IMPL__.dispatchEvent(event);
   },
 
-  getEventType: function getEventType(evt) {
-    let eventType = -1;
-    switch (evt) {
-      case 'peerready':
-        eventType = NFC_PEER_EVENT_READY;
-        break;
-      case 'peerlost':
-        eventType = NFC_PEER_EVENT_LOST;
-        break;
-      default:
-        break;
-    }
-    return eventType;
-  },
-
   hasDeadWrapper: function hasDeadWrapper() {
     return Cu.isDeadWrapper(this._window) || Cu.isDeadWrapper(this.__DOM_IMPL__);
   },
 
   classID: Components.ID("{6ff2b290-2573-11e3-8224-0800200c9a66}"),
   contractID: "@mozilla.org/navigatorNfc;1",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer,
--- a/dom/nfc/tests/marionette/head.js
+++ b/dom/nfc/tests/marionette/head.js
@@ -121,18 +121,19 @@ let NCI = (function() {
     LIMIT_NOTIFICATION: 1,
     MORE_NOTIFICATIONS: 2
   };
 }());
 
 let TAG = (function() {
   function setData(re, flag, tnf, type, payload) {
     let deferred = Promise.defer();
+    let tnfNum = NDEF.getTNFNum(tnf);
     let cmd = "nfc tag set " + re +
-              " [" + flag + "," + tnf + "," + type + ",," + payload + "]";
+              " [" + flag + "," + tnfNum + "," + type + ",," + payload + "]";
 
     emulator.run(cmd, function(result) {
       is(result.pop(), "OK", "set NDEF data of tag" + re);
       deferred.resolve();
     });
 
     return deferred.promise;
   };
@@ -151,18 +152,19 @@ let TAG = (function() {
     setData: setData,
     clearData: clearData
   };
 }());
 
 let SNEP = (function() {
   function put(dsap, ssap, flags, tnf, type, id, payload) {
     let deferred = Promise.defer();
+    let tnfNum = NDEF.getTNFNum(tnf);
     let cmd = "nfc snep put " + dsap + " " + ssap + " [" + flags + "," +
-                                                           tnf + "," +
+                                                           tnfNum + "," +
                                                            type + "," +
                                                            id + "," +
                                                            payload + "]";
     emulator.run(cmd, function(result) {
       is(result.pop(), "OK", "send SNEP PUT");
       deferred.resolve();
     });
 
@@ -240,17 +242,28 @@ function runTests() {
     // succeed immediately on systems without NFC
     log('Skipping test on system without NFC');
     ok(true, 'Skipping test on system without NFC');
     finish();
   }
 }
 
 const NDEF = {
-  TNF_WELL_KNOWN: 1,
+  TNF_WELL_KNOWN: "well-known",
+
+  tnfValues: ["empty", "well-known", "media-type", "absolute-uri", "external",
+    "unknown", "unchanged", "reserved"],
+
+  getTNFNum: function (tnfString) {
+    return this.tnfValues.indexOf(tnfString);
+  },
+
+  getTNFString: function(tnfNum) {
+    return this.tnfValues[tnfNum];
+  },
 
   // compares two NDEF messages
   compare: function(ndef1, ndef2) {
     isnot(ndef1, null, "LHS message is not null");
     isnot(ndef2, null, "RHS message is not null");
     is(ndef1.length, ndef2.length,
        "NDEF messages have the same number of records");
     ndef1.forEach(function(record1, index) {
@@ -285,17 +298,17 @@ const NDEF = {
       ok(false, "Parser error: " + e.message);
       return null;
     }
     // and build NDEF array
     let ndef = arr.map(function(value) {
         let type = NfcUtils.fromUTF8(this.atob(value.type));
         let id = NfcUtils.fromUTF8(this.atob(value.id));
         let payload = NfcUtils.fromUTF8(this.atob(value.payload));
-        return new MozNDEFRecord({tnf: value.tnf, type: type, id: id, payload: payload});
+        return new MozNDEFRecord({tnf: NDEF.getTNFString(value.tnf), type: type, id: id, payload: payload});
       }, window);
     return ndef;
   }
 };
 
 var NfcUtils = {
   fromUTF8: function(str) {
     let buf = new Uint8Array(str.length);
--- a/dom/nfc/tests/marionette/test_ndef.js
+++ b/dom/nfc/tests/marionette/test_ndef.js
@@ -3,26 +3,54 @@
 
 MARIONETTE_TIMEOUT = 30000;
 MARIONETTE_HEAD_JS = 'head.js';
 
 function testConstructNDEF() {
   try {
     // omit type, id and payload.
     let r = new MozNDEFRecord();
-    is(r.tnf, 0, "r.tnf should be 0");
+    is(r.tnf, "empty", "r.tnf should be 'empty'");
     is(r.type, null, "r.type should be null");
     is(r.id, null, "r.id should be null");
     is(r.payload, null, "r.payload should be null");
 
     ok(true);
   } catch (e) {
     ok(false, 'type, id or payload should be optional. error:' + e);
   }
 
+  try {
+    new MozNDEFRecord({type: new Uint8Array(1)});
+    ok(false, "new MozNDEFRecord should fail, type should be null for empty tnf");
+  } catch (e){
+    ok(true);
+  }
+
+  try {
+    new MozNDEFRecord({tnf: "unknown", type: new Uint8Array(1)});
+    ok(false, "new MozNDEFRecord should fail, type should be null for unknown tnf");
+  } catch (e){
+    ok(true);
+  }
+
+  try {
+    new MozNDEFRecord({tnf: "unchanged", type: new Uint8Array(1)});
+    ok(false, "new MozNDEFRecord should fail, type should be null for unchanged tnf");
+  } catch (e){
+    ok(true);
+  }
+
+  try {
+    new MozNDEFRecord({tnf: "illegal", type: new Uint8Array(1)});
+    ok(false, "new MozNDEFRecord should fail, invalid tnf");
+  } catch (e){
+    ok(true);
+  }
+
   runNextTest();
 }
 
 let tests = [
   testConstructNDEF
 ];
 
 runTests();
--- a/dom/nfc/tests/marionette/test_nfc_error_messages.js
+++ b/dom/nfc/tests/marionette/test_nfc_error_messages.js
@@ -5,17 +5,17 @@
 
 /* globals log, is, ok, runTests, toggleNFC, runNextTest,
    SpecialPowers, nfc, MozNDEFRecord, emulator */
 
 const MARIONETTE_TIMEOUT = 60000;
 const MARIONETTE_HEAD_JS = 'head.js';
 
 const MANIFEST_URL = 'app://system.gaiamobile.org/manifest.webapp';
-const NDEF_MESSAGE = [new MozNDEFRecord({tnf: 0x01,
+const NDEF_MESSAGE = [new MozNDEFRecord({tnf: "well-known",
                                          type: new Uint8Array(0x84),
                                          payload: new Uint8Array(0x20)})];
 
 let nfcPeers = [];
 
 /**
  * Enables nfc and RE0 then registers onpeerready callback and once
  * it's fired it creates mozNFCPeer and stores it for later.
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -3465,24 +3465,16 @@ RilObject.prototype = {
                                 cardState: this.cardState});
 
         this.iccInfo = {iccType: null};
         this.context.ICCUtilsHelper.handleICCInfoChange();
       }
       return;
     }
 
-    let ICCRecordHelper = this.context.ICCRecordHelper;
-    // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED.
-    if (iccStatus.cardState === CARD_STATE_PRESENT &&
-        (this.cardState === GECKO_CARDSTATE_UNINITIALIZED ||
-         this.cardState === GECKO_CARDSTATE_UNDETECTED)) {
-      ICCRecordHelper.readICCID();
-    }
-
     if (RILQUIRKS_SUBSCRIPTION_CONTROL) {
       // All appIndex is -1 means the subscription is not activated yet.
       // Note that we don't support "ims" for now, so we don't take it into
       // account.
       let neetToActivate = iccStatus.cdmaSubscriptionAppIndex === -1 &&
                            iccStatus.gsmUmtsSubscriptionAppIndex === -1;
       if (neetToActivate &&
           // Note: setUiccSubscription works abnormally when RADIO is OFF,
@@ -3533,16 +3525,24 @@ RilObject.prototype = {
       if (pin1State === CARD_PINSTATE_ENABLED_PERM_BLOCKED) {
         newCardState = GECKO_CARDSTATE_PERMANENT_BLOCKED;
       }
     } else {
       // Having incorrect app information, set card state to unknown.
       newCardState = GECKO_CARDSTATE_UNKNOWN;
     }
 
+    let ICCRecordHelper = this.context.ICCRecordHelper;
+    // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED.
+    if (iccStatus.cardState === CARD_STATE_PRESENT &&
+        (this.cardState === GECKO_CARDSTATE_UNINITIALIZED ||
+         this.cardState === GECKO_CARDSTATE_UNDETECTED)) {
+      ICCRecordHelper.readICCID();
+    }
+
     if (this.cardState == newCardState) {
       return;
     }
 
     // This was moved down from CARD_APPSTATE_READY
     this.requestNetworkInfo();
     if (newCardState == GECKO_CARDSTATE_READY) {
       // For type SIM, we need to check EF_phase first.
@@ -5849,17 +5849,21 @@ RilObject.prototype[REQUEST_SIM_IO] = fu
     return;
   }
 
   // Don't need to read rilRequestError since we can know error status from
   // sw1 and sw2.
   let Buf = this.context.Buf;
   options.sw1 = Buf.readInt32();
   options.sw2 = Buf.readInt32();
-  if (options.sw1 != ICC_STATUS_NORMAL_ENDING) {
+  // See 3GPP TS 11.11, clause 9.4.1 for opetation success results.
+  if (options.sw1 !== ICC_STATUS_NORMAL_ENDING &&
+      options.sw1 !== ICC_STATUS_NORMAL_ENDING_WITH_EXTRA &&
+      options.sw1 !== ICC_STATUS_WITH_SIM_DATA &&
+      options.sw1 !== ICC_STATUS_WITH_RESPONSE_DATA) {
     ICCIOHelper.processICCIOError(options);
     return;
   }
   ICCIOHelper.processICCIO(options);
 };
 RilObject.prototype[REQUEST_SEND_USSD] = function REQUEST_SEND_USSD(length, options) {
   if (DEBUG) {
     this.context.debug("REQUEST_SEND_USSD " + JSON.stringify(options));
@@ -12700,16 +12704,17 @@ ICCIOHelperObject.prototype = {
         options.p3 = 0x00;
         break;
       // For RUIM, CSIM and ISIM, cf bug 955946: keep the old behavior
       case CARD_APPTYPE_RUIM:
       case CARD_APPTYPE_CSIM:
       case CARD_APPTYPE_ISIM:
       // For SIM, this is what we want
       case CARD_APPTYPE_SIM:
+      default:
         options.p2 = 0x00;
         options.p3 = GET_RESPONSE_EF_SIZE_BYTES;
         break;
     }
     this.context.RIL.iccIO(options);
   },
 
   /**
--- a/dom/webidl/MozNDEFRecord.webidl
+++ b/dom/webidl/MozNDEFRecord.webidl
@@ -1,31 +1,33 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Copyright © 2013 Deutsche Telekom, Inc. */
 
+enum TNF {
+  "empty",
+  "well-known",
+  "media-type",
+  "absolute-uri",
+  "external",
+  "unknown",
+  "unchanged"
+};
+
 [Constructor(optional MozNDEFRecordOptions options)]
 interface MozNDEFRecord
 {
   /**
-   * Type Name Field (3-bits) - Specifies the NDEF record type in general.
-   *   tnf_empty: 0x00
-   *   tnf_well_known: 0x01
-   *   tnf_mime_media: 0x02
-   *   tnf_absolute_uri: 0x03
-   *   tnf_external type: 0x04
-   *   tnf_unknown: 0x05
-   *   tnf_unchanged: 0x06
-   *   tnf_reserved: 0x07
+   * Type Name Field - Specifies the NDEF record type in general.
    */
   [Constant]
-  readonly attribute octet tnf;
+  readonly attribute TNF tnf;
 
   /**
    * type - Describes the content of the payload. This can be a mime type.
    */
   [Constant]
   readonly attribute Uint8Array? type;
 
   /**
@@ -38,13 +40,13 @@ interface MozNDEFRecord
    * payload - Binary data blob. The meaning of this field is application
    * dependent.
    */
   [Constant]
   readonly attribute Uint8Array? payload;
 };
 
 dictionary MozNDEFRecordOptions {
-  octet tnf = 0; // default to tnf_empty.
+  TNF tnf = "empty";
   Uint8Array type;
   Uint8Array id;
   Uint8Array payload;
 };
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -959,17 +959,17 @@ fails == 413027-3.html 413027-3-ref.html
 == 413286-2b.html 413286-2-ref.html
 == 413286-2c.html 413286-2-ref.html
 == 413286-3.html 413286-3-ref.html
 == 413286-4a.html 413286-4-ref.html
 == 413286-4b.html 413286-4-ref.html
 == 413286-5.html 413286-5-ref.html
 == 413286-6.html 413286-6-ref.html
 skip-if(cocoaWidget) == 413292-1.html 413292-1-ref.html # disabling due to failure loading on some mac tinderboxes. See bug 432954
-== 413361-1.html 413361-1-ref.html
+fuzzy-if(Android&&AndroidVersion>=15,11,15) == 413361-1.html 413361-1-ref.html
 == 413840-background-unchanged.html 413840-background-unchanged-ref.html
 == 413840-ltr-offsets.html 413840-ltr-offsets-ref.html
 == 413840-rtl-offsets.html 413840-rtl-offsets-ref.html
 == 413840-pushed-line-bullet.html 413840-pushed-line-bullet-ref.html
 == 413840-bullet-first-line.html 413840-bullet-first-line-ref.html
 == 413982.html 413982-ref.html
 == 414123.xhtml 414123-ref.xhtml
 == 414638.html 414638-ref.html
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1515,23 +1515,22 @@ public class BrowserApp extends GeckoApp
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     @Override
     public void addTab() {
-        // Always load about:home when opening a new tab.
-        Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
+        Tabs.getInstance().addTab();
     }
 
     @Override
     public void addPrivateTab() {
-        Tabs.getInstance().loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
+        Tabs.getInstance().addPrivateTab();
     }
 
     @Override
     public void showNormalTabs() {
         showTabs(TabsPanel.Panel.NORMAL_TABS);
     }
 
     @Override
@@ -2552,22 +2551,22 @@ public class BrowserApp extends GeckoApp
         if (AboutPages.isAboutReader(url)) {
             String urlFromReader = ReaderModeUtils.getUrlFromAboutReader(url);
             if (urlFromReader != null) {
                 url = urlFromReader;
             }
         }
 
         // Disable share menuitem for about:, chrome:, file:, and resource: URIs
-        final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
-        share.setVisible(!inGuestMode);
-        share.setEnabled(StringUtils.isShareableUrl(url) && !inGuestMode);
-        MenuUtils.safeSetEnabled(aMenu, R.id.apps, !inGuestMode);
-        MenuUtils.safeSetEnabled(aMenu, R.id.addons, !inGuestMode);
-        MenuUtils.safeSetEnabled(aMenu, R.id.downloads, !inGuestMode);
+        final boolean shareEnabled = RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_SHARE);
+        share.setVisible(shareEnabled);
+        share.setEnabled(StringUtils.isShareableUrl(url) && shareEnabled);
+        MenuUtils.safeSetEnabled(aMenu, R.id.apps, RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_INSTALL_APPS));
+        MenuUtils.safeSetEnabled(aMenu, R.id.addons, RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_INSTALL_EXTENSIONS));
+        MenuUtils.safeSetEnabled(aMenu, R.id.downloads, RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_DOWNLOADS));
 
         // NOTE: Use MenuUtils.safeSetEnabled because some actions might
         // be on the BrowserToolbar context menu.
         if (Versions.feature11Plus) {
             MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab));
         }
         MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds());
         MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -99,17 +99,16 @@ import android.net.NetworkInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
 import android.os.SystemClock;
-import android.os.UserManager;
 import android.os.Vibrator;
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
@@ -2549,49 +2548,16 @@ public class GeckoAppShell
                 return "PROXY " + proxy.address().toString();
             case SOCKS:
                 return "SOCKS " + proxy.address().toString();
         }
 
         return "DIRECT";
     }
 
-    @WrapElementForJNI
-    public static boolean isUserRestricted() {
-        if (Versions.preJBMR2) {
-            return false;
-        }
-
-        UserManager mgr = (UserManager)getContext().getSystemService(Context.USER_SERVICE);
-        Bundle restrictions = mgr.getUserRestrictions();
-
-        return !restrictions.isEmpty();
-    }
-
-    @WrapElementForJNI
-    public static String getUserRestrictions() {
-        if (Versions.preJBMR2) {
-            return "{}";
-        }
-
-        JSONObject json = new JSONObject();
-        UserManager mgr = (UserManager)getContext().getSystemService(Context.USER_SERVICE);
-        Bundle restrictions = mgr.getUserRestrictions();
-
-        Set<String> keys = restrictions.keySet();
-        for (String key : keys) {
-            try {
-                json.put(key, restrictions.get(key));
-            } catch (JSONException e) {
-            }
-        }
-
-        return json.toString();
-    }
-
     /* Downloads the uri pointed to by a share intent, and alters the intent to point to the locally stored file.
      */
     public static void downloadImageForIntent(final Intent intent) {
         final String src = intent.getStringExtra(Intent.EXTRA_TEXT);
         final File dir = GeckoApp.getTempDirectory();
 
         if (dir == null) {
             showImageShareFailureToast();
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/RestrictedProfiles.java
@@ -0,0 +1,132 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.util.Log;
+
+import java.lang.StringBuilder;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
+
+
+public class RestrictedProfiles {
+    private static final String LOGTAG = "GeckoRestrictedProfiles";
+
+    // These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlServices.java
+    public static enum Restriction {
+        DISALLOW_DOWNLOADS(1, "no_download_files"),
+        DISALLOW_INSTALL_EXTENSIONS(2, "no_install_extensions"),
+        DISALLOW_INSTALL_APPS(3, UserManager.DISALLOW_INSTALL_APPS),
+        DISALLOW_BROWSE_FILES(4, "no_browse_files"),
+        DISALLOW_SHARE(5, "no_share"),
+        DISALLOW_BOOKMARK(6, "no_bookmark"),
+        DISALLOW_ADD_CONTACTS(7, "no_add_contacts"),
+        DISALLOW_SET_IMAGE(8, "no_set_image");
+
+        public final int id;
+        public final String name;
+
+        private Restriction(final int id, final String name) {
+            this.id = id;
+            this.name = name;
+        }
+    }
+
+    private static String geckoActionToRestrction(int action) {
+        for (Restriction rest : Restriction.values()) {
+            if (rest.id == action) {
+                return rest.name;
+            }
+        }
+
+        throw new IllegalArgumentException("Unknown action " + action);
+    }
+
+    private static Bundle getRestrctions() {
+        final UserManager mgr = (UserManager) GeckoAppShell.getContext().getSystemService(Context.USER_SERVICE);
+        return mgr.getUserRestrictions();
+    }
+
+    @WrapElementForJNI
+    public static boolean isUserRestricted() {
+        // Guest mode is supported in all Android versions
+        if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) {
+            return true;
+        }
+
+        if (Versions.preJBMR2) {
+            return false;
+        }
+
+        return !getRestrctions().isEmpty();
+    }
+
+    public static boolean isAllowed(Restriction action) {
+        return isAllowed(action.id, null);
+    }
+
+    @WrapElementForJNI
+    public static boolean isAllowed(int action, String url) {
+        // ALl actions are blocked in Guest mode
+        if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) {
+            return false;
+        }
+
+        if (Versions.preJBMR2) {
+            return true;
+        }
+
+        try {
+            final String restriction = geckoActionToRestrction(action);
+            return !getRestrctions().getBoolean(restriction, false);
+        } catch(IllegalArgumentException ex) {
+            Log.i(LOGTAG, "Invalid action", ex);
+        }
+
+        return true;
+    }
+
+    @WrapElementForJNI
+    public static String getUserRestrictions() {
+        // Guest mode is supported in all Android versions
+        if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) {
+            StringBuilder builder = new StringBuilder("{ ");
+
+            for (Restriction restriction : Restriction.values()) {
+                builder.append("\"" + restriction.name + "\": true, ");
+            }
+
+            builder.append(" }");
+            return builder.toString();
+        }
+
+        if (Versions.preJBMR2) {
+            return "{}";
+        }
+
+        final JSONObject json = new JSONObject();
+        final Bundle restrictions = getRestrctions();
+        final Set<String> keys = restrictions.keySet();
+
+        for (String key : keys) {
+            try {
+                json.put(key, restrictions.get(key));
+            } catch (JSONException e) {
+            }
+        }
+
+        return json.toString();
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -836,16 +836,24 @@ public class Tabs implements GeckoEventL
         if (AboutPages.isBuiltinIconPage(url)) {
             Log.d(LOGTAG, "Setting about: tab favicon inline.");
             added.updateFavicon(getAboutPageFavicon(url));
         }
 
         return added;
     }
 
+    public Tab addTab() {
+        return loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
+    }
+
+    public Tab addPrivateTab() {
+        return loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
+    }
+
     /**
      * These favicons are only used for the URL bar, so
      * we fetch with that size.
      *
      * This method completes on the calling thread.
      */
     private Bitmap getAboutPageFavicon(final String url) {
         int faviconSize = Math.round(mAppContext.getResources().getDimension(R.dimen.browser_toolbar_favicon_size));
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -364,16 +364,17 @@ gbjar.sources += [
     'prompts/PromptInput.java',
     'prompts/PromptListAdapter.java',
     'prompts/PromptListItem.java',
     'prompts/PromptService.java',
     'prompts/TabInput.java',
     'ReaderModeUtils.java',
     'RemoteTabsExpandableListAdapter.java',
     'Restarter.java',
+    'RestrictedProfiles.java',
     'ScrollAnimator.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
     'SiteIdentity.java',
     'SmsManager.java',
     'sqlite/ByteBufferInputStream.java',
     'sqlite/MatrixBlobCursor.java',
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -669,17 +669,19 @@ OnSharedPreferenceChangeListener
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 } else if (!AppConstants.MOZ_CRASHREPORTER &&
                            PREFS_CRASHREPORTER_ENABLED.equals(key)) {
                     preferences.removePreference(pref);
                     i--;
                     continue;
-                } else if (AppConstants.RELEASE_BUILD && PREFS_GEO_REPORTING.equals(key)) {
+                } else if (AppConstants.RELEASE_BUILD &&
+                            (PREFS_GEO_REPORTING.equals(key) ||
+                             PREFS_GEO_LEARN_MORE.equals(key))) {
                     // We don't build wifi/cell tower collection in release builds, so hide the UI.
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 } else if (PREFS_DEVTOOLS_REMOTE_ENABLED.equals(key)) {
                     final Context thisContext = this;
                     pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                         @Override
--- a/mobile/android/base/preferences/LocaleListPreference.java
+++ b/mobile/android/base/preferences/LocaleListPreference.java
@@ -58,20 +58,17 @@ public class LocaleListPreference extend
             Canvas c = new Canvas(b);
             c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint);
             return b;
         }
 
         private static byte[] getPixels(final Bitmap b) {
             final int byteCount;
             if (Versions.feature19Plus) {
-                // TODO: when Bug 1042829 lands, do the right thing for KitKat devices.
-                // Which is:
-                // byteCount = b.getAllocationByteCount();
-                byteCount = b.getRowBytes() * b.getHeight();
+                byteCount = b.getAllocationByteCount();
             } else {
                 // Close enough for government work.
                 // Equivalent to getByteCount, but works on <12.
                 byteCount = b.getRowBytes() * b.getHeight();
             }
 
             final ByteBuffer buffer = ByteBuffer.allocate(byteCount);
             try {
@@ -267,18 +264,17 @@ public class LocaleListPreference extend
         final String value = getValue();
 
         if (TextUtils.isEmpty(value)) {
             return getContext().getString(R.string.locale_system_default);
         }
 
         // We can't trust super.getSummary() across locale changes,
         // apparently, so let's do the same work.
-        final Locale loc = new Locale(value);
-        return loc.getDisplayName(loc);
+        return new LocaleDescriptor(value).getDisplayName();
     }
 
     private void buildList() {
         final Locale currentLocale = Locale.getDefault();
         Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale);
 
         if (currentLocale.equals(this.entriesLocale) &&
             getEntries() != null) {
--- a/mobile/android/base/tests/testRestrictedProfiles.js
+++ b/mobile/android/base/tests/testRestrictedProfiles.js
@@ -13,33 +13,43 @@ add_task(function test_isUserRestricted(
   do_check_true("@mozilla.org/parental-controls-service;1" in Cc);
   
   let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
   
   // In an admin profile, like the tests: enabled = false
   // In a restricted profile: enabled = true
   do_check_false(pc.parentalControlsEnabled);
 
-  //run_next_test();
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.DOWNLOAD));
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_EXTENSION));
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_APP));
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.VISIT_FILE_URLS));
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.SHARE));
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.BOOKMARK));
+  do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_EXTENSION));
+
+  run_next_test();
 });
-/*
-// NOTE: JNI.jsm has no way to call a string method yet
+
 add_task(function test_getUserRestrictions() {
   // In an admin profile, like the tests: {}
   // In a restricted profile: {"no_modify_accounts":true,"no_share_location":true}
   let restrictions = "{}";
 
-  let jni = null;
+  var jenv = null;
   try {
-    jni = new JNI();
-    let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
-    let method = jni.getStaticMethodID(cls, "getUserRestrictions", "()Ljava/lang/String;");
-    restrictions = jni.callStaticStringMethod(cls, method);
+    jenv = JNI.GetForThread();
+    var geckoAppShell = JNI.LoadClass(jenv, "org.mozilla.gecko.RestrictedProfile", {
+      static_methods: [
+        { name: "getUserRestrictions", sig: "()Ljava/lang/String;" },
+      ],
+    });
+    restrictions = JNI.ReadString(jenv, geckoAppShell.getUserRestrictions());
   } finally {
-    if (jni != null) {
-      jni.close();
+    if (jenv) {
+      JNI.UnloadClasses(jenv);
     }
   }
 
   do_check_eq(restrictions, "{}");
 });
-*/
+
 run_next_test();
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -647,16 +647,20 @@ var SelectionHandler = {
       id: "share_action",
       icon: "drawable://ic_menu_share",
       action: function() {
         SelectionHandler.shareSelection();
         UITelemetry.addEvent("action.1", "actionbar", null, "share");
       },
       selector: {
         matches: function() {
+          if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
+            return false;
+          }
+
           return SelectionHandler.isSelectionActive();
         }
       }
     },
 
     SEARCH: {
       label: function() {
         return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -159,16 +159,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   notifications.forEach(notification => {
     Services.obs.addObserver(observer, notification, false);
   });
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
   "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
 
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+  "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
   "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
 
 XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
   "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
 
 #ifdef MOZ_WEBRTC
 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
@@ -282,17 +285,16 @@ var Strings = {};
 const kFormHelperModeDisabled = 0;
 const kFormHelperModeEnabled = 1;
 const kFormHelperModeDynamic = 2;   // disabled on tablets
 
 var BrowserApp = {
   _tabs: [],
   _selectedTab: null,
   _prefObservers: [],
-  isGuest: false,
 
   get isTablet() {
     let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
     delete this.isTablet;
     return this.isTablet = sysInfo.get("tablet");
   },
 
   get isOnLowMemoryPlatform() {
@@ -429,18 +431,16 @@ var BrowserApp = {
       if (window.arguments[0])
         url = window.arguments[0];
       if (window.arguments[1])
         gScreenWidth = window.arguments[1];
       if (window.arguments[2])
         gScreenHeight = window.arguments[2];
       if (window.arguments[3])
         pinned = window.arguments[3];
-      if (window.arguments[4])
-        this.isGuest = window.arguments[4];
     }
 
     if (pinned) {
       this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl));
     } else {
       SearchEngines.init();
       this.initContextMenu();
     }
@@ -454,17 +454,17 @@ var BrowserApp = {
     // Broadcast a UIReady message so add-ons know we are finished with startup
     let event = document.createEvent("Events");
     event.initEvent("UIReady", true, false);
     window.dispatchEvent(event);
 
     if (this._startupStatus)
       this.onAppUpdated();
 
-    if (this.isGuest) {
+    if (!ParentalControls.isAllowed(ParentalControls.INSTALL_EXTENSIONS)) {
       // Disable extension installs
       Services.prefs.setIntPref("extensions.enabledScopes", 1);
       Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
       Services.prefs.setBoolPref("xpinstall.enabled", false);
     }
 
     // notify java that gecko has loaded
     Messaging.sendRequest({ type: "Gecko:Ready" });
@@ -576,33 +576,33 @@ var BrowserApp = {
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
         NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
       });
 
     NativeWindow.contextmenus.add({
       label: Strings.browser.GetStringFromName("contextmenu.shareLink"),
       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
-      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext),
+      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.linkShareableContext),
       showAsActions: function(aElement) {
         return {
           title: aElement.textContent.trim() || aElement.title.trim(),
           uri: NativeWindow.contextmenus._getLinkURL(aElement),
         };
       },
       icon: "drawable://ic_menu_share",
       callback: function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
       }
     });
 
     NativeWindow.contextmenus.add({
       label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"),
       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
-      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
+      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.emailLinkContext),
       showAsActions: function(aElement) {
         let url = NativeWindow.contextmenus._getLinkURL(aElement);
         let emailAddr = NativeWindow.contextmenus._stripScheme(url);
         let title = aElement.textContent || aElement.title;
         return {
           title: title,
           uri: emailAddr,
         };
@@ -611,58 +611,58 @@ var BrowserApp = {
       callback: function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
       }
     });
 
     NativeWindow.contextmenus.add({
       label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"),
       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
-      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
+      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.phoneNumberLinkContext),
       showAsActions: function(aElement) {
         let url = NativeWindow.contextmenus._getLinkURL(aElement);
         let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
         let title = aElement.textContent || aElement.title;
         return {
           title: title,
           uri: phoneNumber,
         };
       },
       icon: "drawable://ic_menu_share",
       callback: function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
       }
     });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
-      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
+      NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         Messaging.sendRequest({
           type: "Contact:Add",
           email: url
         });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
-      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
+      NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         Messaging.sendRequest({
           type: "Contact:Add",
           phone: url
         });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"),
-      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext),
+      NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         let title = aTarget.textContent || aTarget.title || url;
         Messaging.sendRequest({
           type: "Bookmark:Insert",
           url: url,
@@ -689,17 +689,17 @@ var BrowserApp = {
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
         aTarget.setAttribute("controls", true);
       });
 
     NativeWindow.contextmenus.add({
       label: Strings.browser.GetStringFromName("contextmenu.shareMedia"),
       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
-      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")),
+      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.SelectorContext("video")),
       showAsActions: function(aElement) {
         let url = (aElement.currentSrc || aElement.src);
         let title = aElement.textContent || aElement.title;
         return {
           title: title,
           uri: url,
           type: "video/*",
         };
@@ -737,17 +737,17 @@ var BrowserApp = {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
 
         let url = aTarget.src;
         NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
       });
 
     NativeWindow.contextmenus.add({
       label: Strings.browser.GetStringFromName("contextmenu.shareImage"),
-      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
+      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.imageSaveableContext),
       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
       showAsActions: function(aTarget) {
         let doc = aTarget.ownerDocument;
         let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
                                                          .getImgCacheForDocument(doc);
         let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet);
         let src = aTarget.src;
         return {
@@ -769,17 +769,17 @@ var BrowserApp = {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
 
         ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
                                       false, true, aTarget.ownerDocument.documentURIObject,
                                       aTarget.ownerDocument);
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"),
-      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
+      NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
 
         let src = aTarget.src;
         Messaging.sendRequest({
           type: "Image:SetAs",
           url: src
         });
@@ -2675,21 +2675,23 @@ var NativeWindow = {
         try {
           let url = this._getLinkURL(aElement);
           return Services.io.newURI(url, null, null);
         } catch (e) {}
       }
       return null;
     },
 
-    _disableInGuest: function _disableInGuest(selector) {
+    _disableRestricted: function _disableRestricted(restriction, selector) {
       return {
-        matches: function _disableInGuestMatches(aElement, aX, aY) {
-          if (BrowserApp.isGuest)
+        matches: function _disableRestrictedMatches(aElement, aX, aY) {
+          if (!ParentalControls.isAllowed(ParentalControls[restriction])) {
             return false;
+          }
+
           return selector.matches(aElement, aX, aY);
         }
       };
     },
 
     _getLinkURL: function ch_getLinkURL(aLink) {
       let href = aLink.href;
       if (href)
@@ -4197,18 +4199,18 @@ Tab.prototype = {
 
     this._hostChanged = true;
 
     let fixedURI = aLocationURI;
     try {
       fixedURI = URIFixup.createExposableURI(aLocationURI);
     } catch (ex) { }
 
-    // In guest sessions, we refuse to let you open any file urls.
-    if (BrowserApp.isGuest) {
+    // In restricted profiles, we refuse to let you open any file urls.
+    if (!ParentalControls.isAllowed(ParentalControls.VISIT_FILE_URLS)) {
       let bannedSchemes = ["file", "chrome", "resource", "jar", "wyciwyg"];
 
       if (bannedSchemes.indexOf(fixedURI.scheme) > -1) {
         aRequest.cancel(Cr.NS_BINDING_ABORTED);
 
         aRequest = this.browser.docShell.displayLoadError(Cr.NS_ERROR_UNKNOWN_PROTOCOL, fixedURI, null);
         if (aRequest) {
           fixedURI = aRequest.URI;
--- a/mobile/android/chrome/content/downloads.js
+++ b/mobile/android/chrome/content/downloads.js
@@ -251,21 +251,22 @@ AlertDownloadProgressListener.prototype 
 
     Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [PAUSE_BUTTON, CANCEL_BUTTON]));
   },
 
   onDownloadStateChange: function(aState, aDownload) {
     let state = aDownload.state;
     switch (state) {
       case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: {
-        if (BrowserApp.isGuest) {
+        if (!ParentalControls.isAllowed(ParentalControls.DOWNLOADS)) {
           aDownload.cancel();
           NativeWindow.toast.show(Strings.browser.GetStringFromName("downloads.disabledInGuest"), "long");
           return;
         }
+
         NativeWindow.toast.show(Strings.browser.GetStringFromName("alertDownloadsToast"), "long");
         Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
                                                                          Strings.browser.GetStringFromName("alertDownloadsStart2"),
                                                                          aDownload.displayName));
         break;
       }
       case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: {
         Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [RESUME_BUTTON, CANCEL_BUTTON]));
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -13,34 +13,31 @@ Cu.import("resource://gre/modules/Servic
 function dump(a) {
   Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a);
 }
 
 function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) {
   let argsArray = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
   let urlString = null;
   let pinnedBool = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
-  let guestBool = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
   let widthInt = Cc["@mozilla.org/supports-PRInt32;1"].createInstance(Ci.nsISupportsPRInt32);
   let heightInt = Cc["@mozilla.org/supports-PRInt32;1"].createInstance(Ci.nsISupportsPRInt32);
 
   if ("url" in aArgs) {
     urlString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
     urlString.data = aArgs.url;
   }
   widthInt.data = "width" in aArgs ? aArgs.width : 1;
   heightInt.data = "height" in aArgs ? aArgs.height : 1;
   pinnedBool.data = "pinned" in aArgs ? aArgs.pinned : false;
-  guestBool.data = "guest" in aArgs ? aArgs["guest"] : false;
 
   argsArray.AppendElement(urlString, false);
   argsArray.AppendElement(widthInt, false);
   argsArray.AppendElement(heightInt, false);
   argsArray.AppendElement(pinnedBool, false);
-  argsArray.AppendElement(guestBool, false);
   return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argsArray);
 }
 
 
 function resolveURIInternal(aCmdLine, aArgument) {
   let uri = aCmdLine.resolveURI(aArgument);
   if (uri)
     return uri;
@@ -56,30 +53,26 @@ function resolveURIInternal(aCmdLine, aA
 }
 
 function BrowserCLH() {}
 
 BrowserCLH.prototype = {
   handle: function fs_handle(aCmdLine) {
     let openURL = "about:home";
     let pinned = false;
-    let guest = false;
 
     let width = 1;
     let height = 1;
 
     try {
       openURL = aCmdLine.handleFlagWithParam("url", false);
     } catch (e) { /* Optional */ }
     try {
       pinned = aCmdLine.handleFlag("webapp", false);
     } catch (e) { /* Optional */ }
-    try {
-      guest = aCmdLine.handleFlag("guest", false);
-    } catch (e) { /* Optional */ }
 
     try {
       width = aCmdLine.handleFlagWithParam("width", false);
     } catch (e) { /* Optional */ }
     try {
       height = aCmdLine.handleFlagWithParam("height", false);
     } catch (e) { /* Optional */ }
 
@@ -97,17 +90,16 @@ BrowserCLH.prototype = {
           browserWin.browserDOMWindow.openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
         }
       } else {
         let args = {
           url: openURL,
           pinned: pinned,
           width: width,
           height: height,
-          guest: guest
         };
 
         // Make sure webapps do not have: locationbar, personalbar, menubar, statusbar, and toolbar
         let flags = "chrome,dialog=no";
         if (!pinned)
           flags += ",all";
 
         browserWin = openWindow(null, "chrome://browser/content/browser.xul", "_blank", flags, args);
--- a/mobile/android/locales/en-US/chrome/webapp.properties
+++ b/mobile/android/locales/en-US/chrome/webapp.properties
@@ -55,9 +55,9 @@ installUpdateMessage2=Touch to install u
 retrievalFailedTitle=#1 update failed;#1 updates failed
 
 # LOCALIZATION NOTE (retrievalFailedMessage): Semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # %1$S is a comma-separated list of apps for which retrieval failed.
 # example: Failed to retrieve updates for Foo, Bar, Baz
 retrievalFailedMessage=Failed to retrieve update for %1$S;Failed to retrieve updates for %1$S
 
-webappsDisabledInGuest=Installing apps is disabled in guest sessions
+webappsDisabled=Installing apps is disabled
--- a/mobile/android/modules/WebappManager.jsm
+++ b/mobile/android/modules/WebappManager.jsm
@@ -24,16 +24,19 @@ Cu.import("resource://gre/modules/Task.j
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "Strings", function() {
   return Services.strings.createBundle("chrome://browser/locale/webapp.properties");
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+  "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
 /**
  * Get the formatted plural form of a string.  Escapes semicolons in arguments
  * to provide to the formatter before formatting the string, then unescapes them
  * after getting its plural form, to avoid tripping up the plural form getter
  * with a semicolon in one of the formatter's arguments, since the plural forms
  * of localized strings are delimited by semicolons.
  *
  * Ideally, we'd get the plural form first and then format the string,
@@ -84,18 +87,18 @@ this.WebappManager = {
       DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager);
       return;
     }
 
     this._installApk(aMessage, aMessageManager);
   },
 
   _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
-    if (this.inGuestSession()) {
-      aMessage.error = Strings.GetStringFromName("webappsDisabledInGuest"),
+    if (!ParentalControls.isAllowed(ParentalControls.INSTALL_APPS)) {
+      aMessage.error = Strings.GetStringFromName("webappsDisabled"),
       aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
       return;
     }
 
     let filePath;
 
 
     let appName = aMessage.app.manifest ? aMessage.app.manifest.name
@@ -272,20 +275,16 @@ this.WebappManager = {
       // to ensure the user can always remove an app from the registry (and thus
       // about:apps) even if it's out of sync with installed APKs.
       debug("APK not installed; proceeding directly to removal from registry");
       DOMApplicationRegistry.doUninstall(aData, aMessageManager);
     }
 
   }),
 
-  inGuestSession: function() {
-    return Services.wm.getMostRecentWindow("navigator:browser").BrowserApp.isGuest;
-  },
-
   autoInstall: function(aData) {
     debug("autoInstall " + aData.manifestURL);
 
     // If the app is already installed, update the existing installation.
     // We should be able to use DOMApplicationRegistry.getAppByManifestURL,
     // but it returns a mozIApplication, while _autoUpdate needs the original
     // object from DOMApplicationRegistry.webapps in order to modify it.
     for (let [ , app] in Iterator(DOMApplicationRegistry.webapps)) {
--- a/mobile/android/search/java/org/mozilla/search/Constants.java
+++ b/mobile/android/search/java/org/mozilla/search/Constants.java
@@ -11,16 +11,15 @@
 package org.mozilla.search;
 
 /**
  * Key should not be stored here. For more info on storing keys, see
  * https://github.com/ericedens/FirefoxSearch/issues/3
  */
 public class Constants {
 
-    public static final String POSTSEARCH_FRAGMENT = "org.mozilla.search.POSTSEARCH_FRAGMENT";
-    public static final String PRESEARCH_FRAGMENT = "org.mozilla.search.PRESEARCH_FRAGMENT";
-    public static final String SEARCH_FRAGMENT = "org.mozilla.search.SEARCH_FRAGMENT";
-
     public static final int SUGGESTION_MAX = 5;
 
     public static final String ABOUT_BLANK = "about:blank";
+
+    // TODO: Localize this with region.properties (or a similar solution). See bug 1065306.
+    public static final String DEFAULT_ENGINE_IDENTIFIER = "yahoo";
 }
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -4,51 +4,58 @@
 
 package org.mozilla.search;
 
 import android.app.Activity;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
+import android.provider.Settings;
 import android.support.v4.app.Fragment;
 import android.text.TextUtils;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewStub;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
+import android.widget.ImageView;
 import android.widget.ProgressBar;
+import android.widget.TextView;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.search.providers.SearchEngine;
 import org.mozilla.search.providers.SearchEngineManager;
 
 public class PostSearchFragment extends Fragment {
 
     private static final String LOG_TAG = "PostSearchFragment";
 
     private ProgressBar progressBar;
 
     private SearchEngineManager searchEngineManager;
     private WebView webview;
+    private View errorView;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View mainView = inflater.inflate(R.layout.search_fragment_post_search, container, false);
 
         progressBar = (ProgressBar) mainView.findViewById(R.id.progress_bar);
 
         webview = (WebView) mainView.findViewById(R.id.webview);
         webview.setWebChromeClient(new ChromeClient());
-        webview.setWebViewClient(new LinkInterceptingClient());
+        webview.setWebViewClient(new ResultsWebViewClient());
+
         // This is required for our greasemonkey terror script.
         webview.getSettings().setJavaScriptEnabled(true);
 
         return mainView;
     }
 
     @Override
     public void onDestroyView() {
@@ -72,35 +79,40 @@ public class PostSearchFragment extends 
         searchEngineManager = null;
     }
 
     public void startSearch(final String query) {
         searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() {
             @Override
             public void execute(SearchEngine engine) {
                 final String url = engine.resultsUriForQuery(query);
-                // Only load urls if the url is different than the webview's current url.
-                if (!TextUtils.equals(webview.getUrl(), url)) {
-                    webview.loadUrl(Constants.ABOUT_BLANK);
-                    webview.loadUrl(url);
-                }
+
+                // Load about:blank to avoid flashing old results.
+                webview.loadUrl(Constants.ABOUT_BLANK);
+                webview.loadUrl(url);
             }
         });
     }
 
 
     /**
      * A custom WebViewClient that intercepts every page load. This allows
      * us to decide whether to load the url here, or send it to Android
-     * as an intent.
+     * as an intent. It also handles network errors.
      */
-    private class LinkInterceptingClient extends WebViewClient {
+    private class ResultsWebViewClient extends WebViewClient {
+
+        // Whether or not there is a network error.
+        private boolean networkError;
 
         @Override
         public void onPageStarted(WebView view, final String url, Bitmap favicon) {
+            // Reset the error state.
+            networkError = false;
+
             searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() {
                 @Override
                 public void execute(SearchEngine engine) {
                     // We keep URLs in the webview that are either about:blank or a search engine result page.
                     if (TextUtils.equals(url, Constants.ABOUT_BLANK) || engine.isSearchResultsPage(url)) {
                         // Keeping the URL in the webview is a noop.
                         return;
                     }
@@ -114,16 +126,50 @@ public class PostSearchFragment extends 
 
                     // This sends the URL directly to fennec, rather than to Android.
                     i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
                     i.setData(Uri.parse(url));
                     startActivity(i);
                 }
             });
         }
+
+        @Override
+        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+            Log.e(LOG_TAG, "Error loading search results: " + description);
+
+            networkError = true;
+
+            if (errorView == null) {
+                final ViewStub errorViewStub = (ViewStub) getView().findViewById(R.id.error_view_stub);
+                errorView = errorViewStub.inflate();
+
+                ((ImageView) errorView.findViewById(R.id.empty_image)).setImageResource(R.drawable.network_error);
+                ((TextView) errorView.findViewById(R.id.empty_title)).setText(R.string.network_error_title);
+
+                final TextView message = (TextView) errorView.findViewById(R.id.empty_message);
+                message.setText(R.string.network_error_message);
+                message.setTextColor(getResources().getColor(R.color.network_error_link));
+                message.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        startActivity(new Intent(Settings.ACTION_SETTINGS));
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            // Make sure the error view is hidden if the network error was fixed.
+            if (errorView != null) {
+                errorView.setVisibility(networkError ? View.VISIBLE : View.GONE);
+                webview.setVisibility(networkError ? View.GONE : View.VISIBLE);
+            }
+        }
     }
 
     /**
      * A custom WebChromeClient that allows us to inject CSS into
      * the head of the HTML and to monitor pageload progress.
      *
      * We use the WebChromeClient because it provides a hook to the titleReceived
      * event. Once the title is available, the page will have started parsing the
--- a/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java
@@ -15,17 +15,19 @@ import android.support.v4.content.Cursor
 import android.support.v4.content.Loader;
 import android.support.v4.widget.SimpleCursorAdapter;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.widget.AdapterView;
+import android.widget.ImageView;
 import android.widget.ListView;
+import android.widget.TextView;
 
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation;
 
 /**
@@ -130,16 +132,21 @@ public class PreSearchFragment extends F
     private void updateUiFromCursor(Cursor c) {
         if (c != null && c.getCount() > 0) {
             return;
         }
 
         if (emptyView == null) {
             final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.empty_view_stub);
             emptyView = emptyViewStub.inflate();
+
+            ((ImageView) emptyView.findViewById(R.id.empty_image)).setImageResource(R.drawable.search_fox);
+            ((TextView) emptyView.findViewById(R.id.empty_title)).setText(R.string.search_empty_title);
+            ((TextView) emptyView.findViewById(R.id.empty_message)).setText(R.string.search_empty_message);
+
             listView.setEmptyView(emptyView);
         }
     }
 
     private class SearchHistoryLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
             return new CursorLoader(getActivity(), SEARCH_HISTORY_URI, PROJECTION, null, null,
--- a/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java
@@ -134,17 +134,17 @@ public class SearchPreferenceActivity ex
                     entryValues[i] = engine.getIdentifier();
                 }
 
                 final ListPreference searchEnginePref = (ListPreference) findPreference(PREF_SEARCH_ENGINE_KEY);
                 searchEnginePref.setEntries(entries);
                 searchEnginePref.setEntryValues(entryValues);
 
                 if (searchEnginePref.getValue() == null) {
-                    searchEnginePref.setValue(getResources().getString(R.string.default_engine_identifier));
+                    searchEnginePref.setValue(Constants.DEFAULT_ENGINE_IDENTIFIER);
                 }
                 searchEnginePref.setSummary(searchEnginePref.getEntry());
             }
         };
         task.execute();
     }
 
     private void clearHistory() {
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java
+++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java
@@ -9,16 +9,17 @@ import android.content.SharedPreferences
 import android.os.AsyncTask;
 import android.text.TextUtils;
 import android.util.Log;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.search.Constants;
 import org.mozilla.search.R;
 import org.mozilla.search.SearchPreferenceActivity;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -91,26 +92,34 @@ public class SearchEngineManager impleme
                 if (!TextUtils.isEmpty(identifier)) {
                     try {
                         return createEngine(identifier);
                     } catch (IllegalArgumentException e) {
                         Log.e(LOG_TAG, "Exception creating search engine from pref. Falling back to default engine.", e);
                     }
                 }
 
-                identifier = context.getResources().getString(R.string.default_engine_identifier);
-                return createEngine(identifier);
+                try {
+                    return createEngine(Constants.DEFAULT_ENGINE_IDENTIFIER);
+                } catch (IllegalArgumentException e) {
+                    Log.e(LOG_TAG, "Exception creating search engine from default identifier. " +
+                            "This will happen if the locale doesn't contain the default search plugin.", e);
+                }
+
+                return null;
             }
 
             @Override
             protected void onPostExecute(SearchEngine engine) {
-                // Only touch engine on the main thread.
-                SearchEngineManager.this.engine = engine;
-                if (callback != null) {
-                    callback.execute(engine);
+                if (engine != null) {
+                    // Only touch engine on the main thread.
+                    SearchEngineManager.this.engine = engine;
+                    if (callback != null) {
+                        callback.execute(engine);
+                    }
                 }
             }
         };
         task.execute();
     }
 
     /**
      * Creates a list of SearchEngine instances from all available open search plugins.
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4407781b52670a5429f5bcb9090e240fc15c00ca
GIT binary patch
literal 5333
zc$@*%6e{b9P)<h;3K|Lk000e1NJLTq002z@002Y?1^@s6#0I!{000U>X+uL$Nkc;*
zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DX<Nq|rShJ+?|L<L3^
z5h+$=RKNj8hazJ|6bplbV%G`s5KzX!QA9=M-HdAq@2xfS-kSZ#S>M^`x7XQc?|s+0
z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T
zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0
z#IJ1jr{*iW$(WZW<e?f_&KbNko{YOt-kK%hql^ThT$m-`XQO-vWxZ5MngHeZDAUvU
zoJ;^P6q#Sl=O&?Si84hL8SaVl0ssh<#5ufj4vYCYXr2Igrf1}e1c^yvrV-beY31n1
zX8Q57Q~6>sE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45(
zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r
zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3
z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e
zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB
z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G
z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw
z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d
z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H
z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp
zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s)
z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3)
zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba
z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe
zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf
zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9
z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@><tXJG<r?L)
z%2EcxFktvIQW>R;lZ?BJkMlI<xzFRz+cvLhUjMu)mH8@eDtwh9m1dOzm5-`SRd3Z4
z)t#zss!!A~Y9?x7YT0W0)h?@z&!^9Kp3j|MH2>uMhw8ApiF&yDYW2hFJ?fJhni{?u
z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz
zt39n_sIypSqfWEV6J3%nTQ@<sT(?tqLQhLCSTA3%QSYHXQJ<}!q`ybMTYt*H&>-4i
zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^
z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z
zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$
zF$X<|c!#|X_t<oHD7%Dx)e-CH;keH6jN=C<dnd8eNvGePS<WfW4bGzr3>WYh)GZit
z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz
z&kJ6Nm#<fmSFg8{_hRpA@25UGK8Ze!J`=unzN>vN_+kA5{dW4@^Vjg_`q%qU1ULk&
z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|i<Li|H^g**v03|$raa~LixG^{4<
zdAL=0et35TEn-DPL&UpCkI2%<M~jUXOBQ!V$w$RS)kjT5dqtN;OP5$IS+nFuj9QE!
zracxP8x?ybc5<or(%nmk<Lu%J<L)jqT$Z!!+H$q!smsr<kYB-BaVj1gA06Ki|A`aA
zspU+r^k2Dm<pkH0yNCOd=f*4NjqzRhW&Du@mxQu}(L|TTU5R5!u1OV1;{s1XwcvHK
zU-E(Esg#hEqbW0~(W%X8gtYjy(?TU-im)qPGd(B0FT*sWFhjb^Y1Qsk6QV%TkxVFa
zS!TPKj{Z#bNQ@+#C4*TDvud*5XGdk9%2CV_=Je#6<ZjCy$@9tkel=z_cXemJcK(L^
z!8Pt{4y}dOu3X!>PIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~
zLQ&zpEzVmGY{hI9Z0+4<v#n~|mm*%#^<vB7isDZt+>-0xS$$Xe-OToc?Y*V;rTcf_
zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD
z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y
ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl%
z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX=
znVPf8XG_nK&J~=SIiGia@<PUi@r#KUhdNhuKDxBz(w(lbuHMUmm#<#&xpJx7z5D!C
zm#b&4IbAz_oqfIShW(A!9=o2FU+jKq>9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh-
zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC<Dw@DPb!|OKdt@M_}6Bsz4Yv$
z*I>`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M
ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@<kNR)@201U-mAVp_JRGO`(yOSk?HJD
z_)nFejX!sM3H<VSCT(Ws-}i*``!YINegFUpPf0{URCodHn|o|jMHGi!+EUsrl~UrP
z;Dh2bnpjYTib7HF6%42pMGHhw6r|EpC`3&(KB7EIkqRhktXhn*D%hZ?m{=8x6dxcy
zqLLE-P(!MzAT5u+{7!SXxx3uiy?6HRZYfMMxp!vHoO9-P&&-@Tv+Fsf8>Yg-!v68`
z@soP==<(Cy#fv}I9b!&SPW;fJL+^r5QbR+-8*A6DZ4zD{U4~t?-@JMA1}7vWY(~Fy
z(XqX~ed&@VONyQ93-C!jd-klN-x#Csu9lXTF(oA>e|Ia;0PF%RY@5g9>1zfF;7R~)
zzPT>gy8+lnXs!*Q+jaEl(d-@pmB)1vz={I_lL1jzv~c0V<#q-)bLPxsEcF9u0|Arn
zmmy<vw*n1|Y6t>MVR3PBX^{3)1hBNUv=0Dzw2Dq-bSu!Bo133zP~WQt3>XCf_ncMx
z{Q2{}efsqIkhUDFcBzB+Tg#R$+t95*Td`urzm1KJ*D}6cs(}OCLrm3`=5_%rE-vmv
z+D4h{&F=!(-o1PCsB4ciH*|;h9Xoa$%F4>BWL>%*5YAAHY1oJnBl>OKx^>g+*|Sp;
z6B9Q<J5oU_)o%jWl*-D=Rw+b#_w?|U&uVRL&BXT^grb>z!Azb5c&6}7K_{<d%JMu)
zZlwMo<&I}tTifiVOP792ebB@X*cX@KQ-}tu2;uc2dR#*L)hZg~t^=@XKEQ-7qM+sH
z=cgtmB@IJxHUrN_-f*6MR0`UuhgW~xhd=Gm3UK1HA#;cwdJCEgoa1TKx0%~q2aSdH
zLIm7~qPOzAob2Yh*}x$Dd@%E1tlnqMnw6HCnkvq&=;9>3j;wp=;0Ks6h<Cz8Maz`C
z1Pho<yMT4Eiq3`ldZ#?rtXb0laASM-?)?e6U16s`V|fj0o(}CXfJuLC(2R50ttj#!
zd`9sUevwZ5bp!}-1qB6TS@1uh|IiRT$^mQ!G?su#U&o*o6&3Ykd!7SGGwFY(Fo{;e
z0<^yjfVHEi5(wWIkRdyy&?=K{&}30hPEIa@X(531R&tDNLz)hImXA}>Tc+ogq54BJ
z19}hZ*?rJjf*cYw<7hDlp$lQs7p9@)MzXyla^XY((^7V80W8$CG_#$~hUQ_K-b>{%
zzy(DBPkB6k%<~8wVCERhccKgfN=r-su%m+jhOZ_rhpi1DkSB&>xgl7(0|+}46McuZ
zYeQE8Oa_Hcwg3=UgLUlFV1NP2%jB9suzY3uP6LNmDf{8%R}f-|?nl_qyxuP5NF)^f
zja)@ufmi+zGRE;7j1`@K<j9eXrlzJ->+0&_IXR?A8M=$e!zhnsAU}lCNt{|-3^@%1
z^Uc;U{bVz{5#C#a@SMt2EvLN`tIp|ZnDVPAH6aKC$4w|CyODg&EdaBP@-~TnI%Z_E
zgPr3OI;;k;Ik4>5CFT4H$eSqJI!<+fwqU`6A*@}Mbjq};TTn8TIs(CFp6dasDv}_X
z6=9ahE?Lco&ob8sysu(s6u|tF07sHY$2EQprUJaz0d}4T(-RY232~ySUF#L5Khkma
zp+ko%%F4<PXfX++6CZyuA3xZAKpB9^a$uQ!AtD+_S`tvn5-}kjup~UB4H!VR!{a@e
zJx_qHS|uzR^#ImSB?mQEVkL7`?NX2ajR1Eud?b21%8FKGbWBgoT7q)<Rz$(-P^1dx
zkMi|)2cwHt8>f|v*j?pfB`}im96A=*)x)s3jnL-=^x4f--tZ34a&vRz1`Zr3=-v=O
zq$*ZpS&R5G+f@Xyst35sx%`l*hVS%H3Sje`>LX4);qQ%LGTP|7n_c}nmYw<z(4^8>
zMCB_5i&Ff9broUeuQ^pBn)Hn2Y~)QM{|sPqv!s;(E&<^37;`eYT&Axu>I6*+ETn^&
zvtm8zQZ^?#tz&^N6r|1dC*r#wXaWG{J(6`nJm~}jkOKfV1M7O{L<Goc%*TKxL|EK(
zEbbLvcj0H{5|jVUs&hDXIMe5H@)*tsIUhcQ$Z}U8p(d|n>b;bkc^;%J_XGd(zL)np
zo^?ET*4Nj6Q(j(vnD?kAOVC6E)`dy?_U)^3ODT4w)3D(24ERRaUj+!Nn<6Iw$i>Td
zeAv(7yMZ<5E6N>Sz7r-+X_cTkWn@Kf98ab&pvizV0-DbAn-#qab$=miJ^3YO$4(dc
zS+a--XmYs%Ncku~m1j%>mdLI__NwjMx4&IoUEN|idd*5#L1PLIWC}ixE_vigTa~RL
z1D-@w9>en9qJt|_*A>u=Q20|Q_W)nlx9i#(R1}GJ^ZT+n1P{IlS7z9tVO`^~u2m?~
zKU`%Z-Jg#tmi97nyJGp>hps$r(2Pa?MFiiaD?En%WW%@{-}5sa9khM`gLPeMgpx<=
z2<-xWWOK^nM=-7Y2xf~@eY6&99N*QfT^BkP)|KedKaTta{%E#Sy+U5$DHz^~dGA1I
zhN9hWwRroVlfQu99&#N&%-hEe(m{Ty)8zGf6VlVuQ*d5WSvSu`UM3$b3;MC-3&>$5
z<Sk-~N+{k@(q7Yy5RB|Z$xbAq=-1GH%yaYp{rhY9nR&C5{ytjdp96q$(IQz+8L7TD
zo6mA0Aotj$^&`^;O}OC1PUP`6N{8Yj7q0IE)U$lqTIuU%GF+Yq;4ioTA&+O!eTgNH
z;WuY;40D`94K^f04)>4p#h+9Vic%FQ306yfQP8+n4S0$C9&Q@IEr98H3dVK#Ys131
z><Jq*Q4EW#K+(I+N&-|Z%1v3meEF~D`e?ixk?2LpdJMhH&h`Ox$_B8X!qLMO(2OrE
zS7*afrUk&Bs;#Yk+U+x+(}6MzhM<#t(2LQjl{}8))JIPF1k$?#nlPO^ckb!Edi9d2
z_(&wytw6=?mbpAm;wPlfqSqb(E3@10B;82_P9ktzBH-GZ7VhXJ9FFTUiADPs)*m^d
zox!{Of8Y<^<(~CjH^MsM>M8wW4jS%3PcEt-2B0GJNeMP60F=9cf=Gt4gUR=`#S}Ed
zy4C~ORX&AmJP$)NgCo$JHfSS45;M>M%*%A#0$_3gca*R;2_{a&wRz7`N0*4TLPKuI
zCTj754qzgie)6$j#|Kj#mE2HK$s;D9aqPMhWAQt7DK%6yfSVVm^%+HTM5|*08Y3z)
ztR<|$!DFI?h{9r`(K+g%$!>|BH)-f<*@bm+f@MPlD|I8c+A3vcW{MBy+66M8U7@Xv
zX2+;o8hU1E>Z<DIHb0e|&iYt@)+zjWeTfBVf4U5o+x$YR7j@A1C(19l8~=sW%q9Uh
zRdP`i)f9EmjLQu9cZe$q&^EZj$N`h6gJ$SfVjP7WsA@@CN#x^q%a#aM#soA8hvS&8
z3ujz_-pCJG9u9|xZr_-ICdv{YlY7dYI*J=sI+g!@?$PNVZl9QeCcnvNJ-Pt^Y8(#Y
zFjkrus|bhas`aZOhl5m-*VNQBW@Kbk@CS!BfDq4Kj#$bBz!HSa1+dLZq9`=PES?rP
nHVH@q+#MW?#sN&14tM_#HTw;?6q6qN00000NkvXXu0mjfU1mTz
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ae8a0cb90e7b440457ba02eea6551f76792f373
GIT binary patch
literal 4446
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4b0e99710ecf461969a12c936f5419352292359d
GIT binary patch
literal 6439
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
BSm6Kw
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0f1e2480e3aeb9067a8e497987f9845ba365fa36
GIT binary patch
literal 7438
zc$@(a9r5CcP)<h;3K|Lk000e1NJLTq005W(004&w1^@s6>Ozoj000U>X+uL$Nkc;*
zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DX<Nq|rShJ+?|L<L3^
z5h+$=RKNj8hazJ|6bplbV%G`s5KzX!QA9=M-HdAq@2xfS-kSZ#S>M^`x7XQc?|s+0
z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T
zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0
z#IJ1jr{*iW$(WZW<e?f_&KbNko{YOt-kK%hql^ThT$m-`XQO-vWxZ5MngHeZDAUvU
zoJ;^P6q#Sl=O&?Si84hL8SaVl0ssh<#5ufj4vYCYXr2Igrf1}e1c^yvrV-beY31n1
zX8Q57Q~6>sE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45(
zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r
zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3
z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e
zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB
z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G
z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw
z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d
z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H
z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp
zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s)
z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3)
zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba
z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe
zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf
zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9
z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@><tXJG<r?L)
z%2EcxFktvIQW>R;lZ?BJkMlI<xzFRz+cvLhUjMu)mH8@eDtwh9m1dOzm5-`SRd3Z4
z)t#zss!!A~Y9?x7YT0W0)h?@z&!^9Kp3j|MH2>uMhw8ApiF&yDYW2hFJ?fJhni{?u
z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz
zt39n_sIypSqfWEV6J3%nTQ@<sT(?tqLQhLCSTA3%QSYHXQJ<}!q`ybMTYt*H&>-4i
zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^
z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z
zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$
zF$X<|c!#|X_t<oHD7%Dx)e-CH;keH6jN=C<dnd8eNvGePS<WfW4bGzr3>WYh)GZit
z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz
z&kJ6Nm#<fmSFg8{_hRpA@25UGK8Ze!J`=unzN>vN_+kA5{dW4@^Vjg_`q%qU1ULk&
z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|i<Li|H^g**v03|$raa~LixG^{4<
zdAL=0et35TEn-DPL&UpCkI2%<M~jUXOBQ!V$w$RS)kjT5dqtN;OP5$IS+nFuj9QE!
zracxP8x?ybc5<or(%nmk<Lu%J<L)jqT$Z!!+H$q!smsr<kYB-BaVj1gA06Ki|A`aA
zspU+r^k2Dm<pkH0yNCOd=f*4NjqzRhW&Du@mxQu}(L|TTU5R5!u1OV1;{s1XwcvHK
zU-E(Esg#hEqbW0~(W%X8gtYjy(?TU-im)qPGd(B0FT*sWFhjb^Y1Qsk6QV%TkxVFa
zS!TPKj{Z#bNQ@+#C4*TDvud*5XGdk9%2CV_=Je#6<ZjCy$@9tkel=z_cXemJcK(L^
z!8Pt{4y}dOu3X!>PIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~
zLQ&zpEzVmGY{hI9Z0+4<v#n~|mm*%#^<vB7isDZt+>-0xS$$Xe-OToc?Y*V;rTcf_
zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD
z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y
ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl%
z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX=
znVPf8XG_nK&J~=SIiGia@<PUi@r#KUhdNhuKDxBz(w(lbuHMUmm#<#&xpJx7z5D!C
zm#b&4IbAz_oqfIShW(A!9=o2FU+jKq>9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh-
zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC<Dw@DPb!|OKdt@M_}6Bsz4Yv$
z*I>`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M
ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@<kNR)@201U-mAVp_JRGO`(yOSk?HJD
z_)nFejX!sM3H<VSCT(Ws-}i*``!YINegFUxh)G02RCodHoe7W?#Q}!pT988ygOoxd
zhmeX#h(I`vA~7)<PbwQRDN9UBB8z~?mC^*2lqk1^%b>=XRHA0h8ci9?1PoHe$`ME@
zMwIAs#3M@-P<A0K3*`HkH|yKox4ZLZdS?3VOjmVvzd5@9{`;T4yI-?Y5|a}Q%$PA_
zP_t&u9yoF0gxrsgA3t7HSy_4S;>C*(N0ESu6DPKA+qUg2@Z^K%3@~iLfA_q3^ERa8
zX_o#Y@!QG3^y$;bl8KLEThv0h?bxwnmn~SZU~jGKf&QE@VZ!O{+qeHKkz80SqT`5c
zJR)1=Tu%h%Fkz`HaW!w={4wA)&jgVS{U@c_sIWwq64x5A^sfic39jGfh%6CUCYCI@
z)|j{)m|=;mF>$5gsTNrxFozY(T(!A?v>`*am=S5vB6BOQG;qi9e=TFNoAyxRjfeq+
z^?+M(2_ny)M^*arv}x1S;5KSJh)eM|NAwRS0yCNw<^m&gRDxw?f)JTUaaG|4&%i`r
zS!H2(50g0`d5O)W$oz;)*!f64P6Xy97ejbQ<6K^&F(ER);u3an6eR-lnu`Iyr$JPr
z*yc4FKO!?BE^xnf@ZiCpBm(oAg8{#%5qb@xs(0u$DlZ~4Auce#edNfIQ7cxgC`*PZ
zyyjj=@2OX)-l4?eHA;!p&0wr4aaB}QT>8*M4}B=SiNL((T8Qt3A+jLEl?W_Ek@$@&
zNRb64u0&vdb3NqugAiGe;)=DvwA>^wFRvHvo8A!72it>oP8V!v{8g5=ODBG}!EJ?`
z$Mq`9Z_Tw;aP27G3i+`|u%B>kAJ_Kb-;do#+vjIYM3>_?VoZwc@ZrP9SQSz4#I+m1
zr1kCeHdpO;nsgg%%9JTxTC`}<7u$!7?oH;)_JI_MsyqHb(@#7HSyj-HxFkqp#+1lN
zekGtXu8MF2wlQ%jj6ex?1Jd9#&1TM=c`o4%PqY0DhM#p%WI!<j8<F*kE?zJpG8fU*
z`-mHH>E)L7hG6*(RvUDnR2srpI2=1n7RV|sY%;G01xD+?5ZNfdLl|E0xfPef&MASh
zKI$t7X*9U6#{Du?pX~$1uX)Kpx9>zDvTVeqbl5I1ReQrolteWeQT1|5z9rAS8#InE
zMV76&ly>U`Hge?1mV*Wj8cFFSQC){U6JZH!&Kah94uk<Z43T*em(p&Hz-Utq2G4ES
zYY|izfy;G6rhF1s8iEv=S8*xrkroYO$Bu2&wQJWg2x<a0>jidUWuDh~D8afZ<GsG`
z<vr8AOF!Zg{zwFN#~pX{Y}KmOczQrT1jFfGh|Gm|4jnqwX6e$U8h{Pem=c*^aVgPZ
zR*`D3f0H;TV84yEy$1+He}?r?k$k|Z5qb@x`Zbj;TedvrCy>AiP@{dwkRiA5HB6@(
z?CS??uDwf|yED-GL72Er%q^zlB@M2Y`lP>;?hmpQQHeEX@ZiC365*=2iAyAtX3c4a
zW=xAqz)6Fz-AaK7i~-TB5Z6Dz_hcM}m8PiB1Qvw2oOJjRnNfiutV<YfSWn~qPk{40
ziRvUQ*Ijy%p$IHUaXES5Rb)m4rd`OCST7;0wE)p%b7EisF1iIxL>S_7(%?m8z63UP
z>eSC6u3r=JW<aQin3yyM;N`-!pT@mmipv3?t;jqJ3}Jnr>74CEc#{)(;?`jxu)*Uf
z#O0(e8<Ax_Orb$iR)Vf1l5aT?CT?a1=;9waaNt0PRjXDV&HRM7t0u$+wzrABQeyFj
zqH5=MqeB?3OR)U>{1!Cgry#C35^=f95_oFev17*z+&%5?DO2LoGG}dN(>RyAnCm_z
zacVu9W)AA!J$c2|NdBF7-r1k2okw`(tMvtxi`OAnqttt_C0H%3k-L}cWw_;JK$R;$
z&4DsT=F90BmhIoa|M=NwpWOz3Yo2Y*{np%X!>D%`u64zK2G>Qp_B6J^HDXMP%h|W+
zchO>+$TA*+*I*SnZrr$5ojZ5F7j|Y~U5U{MljZ{{Bf~cm>{e`*nD||?dGqGoYuBzd
zF{NT2ajg)85SJ5$iMdPo%vfF4D=-@IJpk}D3eCk~2Z^YdM8AqH-nDDjw!*?fUvp|s
zW<=ozDJ~~HL5VD*z!29r5Y}&rysHDia(5R(`Xg1^x}!&r7B5+{q||b}uHl6tE(c5y
zBC9R1=7{U3h-(&+y6ylt0K;?<)OO-4!Y!IVf4-`|CTGGFmqVUukyQ&!gQhf6RchHh
zV7i4;sz`R>!i8@+q%=i0bq@Wjq|NhSEgxTln_}zZ&5+36V2EZ&mB0va7`>g}Q*x=@
zWt;ggL_JEkdKEpNUABP>3J+6n_Y?MAUc>h)E+;-iB3syuDsdcvE<;?coWN~xA!q|7
z)pE|6DvV-PXw)2#FFVMSzFu(n6_*pI0g>%#NhX#sZc|-VI)O*z{tt4VWGrbpRp6G0
zFjx^S;;MJ>vLS$`#HBd=ZX^Lq*X9JW8qeg0KnHz~_&>t_m}*cfKz|T-ai#dq`U8VQ
zPi?M-@>e3G5b`?WzqPvLqDWrOS5weXWTx*y$U(wfgk6ZR<WEeB0rF!g`LUj#u6px9
zh$}ravaK$`bAp;^Wc8mbF%|Te9zA+!P_=h@zybMH6)pilK3~Y6*RNk+5l}oi8!Vi>
zH)6zy|B!&I5L$IXkSReyi%W497Z;Zg8#e42pj?S{Q+-b&ZwqSxA3uJ4=hIF*Z3E%;
z%ajlU*D4|7mkfr^=8)U>4TRU^gGhD49<EPhVTdc8WH%)lRq7<hCYS2H8UoV=Cbs)g
znn`4)m-{|P8*3WEdPngj#ytZP83J3$8?~#;w{)7bGjZ)cM1N1FM79P@=ck8+*mVxt
zB$_+Roa=Q2rW*us89mDv@%o<NCQcRe6N&U0g`F603@|P>94;RuQwQRHj9a90=yg=M
zw-Eik#X^X-ojP@z4UT;9Xi9M_O`f~xpsh;}IitYz0AEc3FE3AjI2%8aO3jZ^YE9=0
z3*``yZ1Cmqv&u80Cng5TMZE%3q=?LG&vFO#@Hpz>SAqnegl#_q)xF3}P_yk>QfM8e
z)SFvcT6!Zzw-)dvCguz@Sb~Y0EP9rE5LiBKtxYlKG)aCM1GRf4X}3;#SFT*SpJ9co
z`L(~!&C~^i^&)Mp3lnkG$54E)H(G)@)MV;eA}p;Ey$4~<!OfY5r<10Xs|kOdqS%}C
z^pVST`E_P~d;IP2SGoLl{MuBjW!UB1FPC2~zsvCJjQkS(CHUW!3qShbo?PVz=-!QM
zfhmggo+ZR-j_O86YhU&reYkIGjTL49OOvWjrY80;k&a#n;nX0J5XWKS*hw7Q@xKah
z#f)EVjl(LDtOTY^P|p%aTd~IQWqzP*wi>MnZP%{dCE(K(nCt*DTDyQEHwaA!(`hAc
zv21yH`76tpFaJ1lV1@x?Eij$;B@DLi#^D1vi_E*8Qff5rB?zj)p0Z&+{RF4oP3tJ#
ziopD=?H{k(gk~);w*iOw92<hwcB(q+oOt!;3^|=HJ`A<jw9U@hbC9z`J~^8a+G)<_
z*K3&4x}A&|g>A~_Wu{@4?cTln`CQw6mvaJ3O|;&Mntnt<_4%f(s@iFzVEZ@MenB<*
zh!z6YzHj62oDrCgFJRY`&Uwv;$UGa#3=vZWVk<;w^AOtWA;z0^RMrcO_Eld*bss6y
zdHUAl%{m8sypU?EJ%xJ@^S=M`5maEWTAN|?2DOE57Mi*R*Y~oaz!<b6U@hc)94NEt
zeZFT0q?0JD6PW%)lu7ox5tjA`w6Te-9KfQb=qs4oyPt)fno)?{SS2tP)s2CmC5X$-
zn(xTTvlRdw?uEn2bLY-|Dr$^Y2n=C;0U}ml^$#G4F~`6Q1XY00_LvJYyO5$D<!(+R
z{&v2$B4D4N9rWaB7Yy{{nOlbsA6~|;pugkS0B)!zvJ#B(tX@p#{0?yq4K>9vjf`qj
zM=^YlVU3+(QV@y2=pkN1BA-B9ox)6P+~Y!I+8c2*J<aFC;v!UmX?mw;&z^G-mIh2i
zjXNwtZI1_$$;{`}$GgIqb_y+%IsW`i)?%qwbFuB25EFdsSd%s;=slI82#oetJ0_)d
zE_?TwWL~Z$^L-}Kzeu}GD{})ejjqWz5vvkFu{_5_SV<V>7*L)BDJ~%mr37;*(P~(l
zUZI}g<HX&V3)%a?R<(xSap`sK<oYMPTjQL3g++<nzC(u&?I;19_4*y@75C!Z>b3p6
zt95Et{=q%iR?yg=&=@a%?}s|aw`tR+$^84R#%3<_3*689kgLrq+pxv52&vdZ_g%ZJ
z>fLF@y(*I$j&YW3%e?^6MLqtwT6vLz7^o9!38s`}vCq<U!c`KzfS8I{|Nc@`XF3!V
z6tribz~x%f4Ia&0bTpmNM#9_i>8GC#)po>2o-=Ygbb$$VfG7CWAcc}jhuZ1SB#KH(
zO4iUVuc+}r#6K9e$?M&__t(KGvE2a94go{b9@n9Z7A@Kla6Hw)M<OuAPSs)T1fS~E
zeCBzROw;bC-_Xv;iIWp)pGsUeAu_d#&iyPY#(b5u7q6fxu-aINE&_^7V8RBezE1E-
zPalHPJlk{FpChWjrQgdnKfGKD=1JIV5uA}cf}RQRq6Gz^8YAHSAU4snguIvdI`3|3
zhX?cL7z638-jBvE_ro##gwkWogfVh~X`dxMOC5HXb<Yyw)+4Mshn{6hXf9tvXcr?i
zO|gyekdD<GH*UNls(q}H3rvK0=vkK18=TBK^rt-#p0ls8wCM(VpG!zUH<yVBwa1tV
zQz#?(nIcZV=H>(+=Xpx4$2rO8+_;HL=~tSSc5&eF(STcZDpFX(5%uA)q6BlmHPW*z
zp*p;kUQo^)w&J8K2p6tKQElp9h<j%UKaw6}1{|kEJ)&F#6RR#uI2&FE+}2(?{f9$`
z4qeI2f0O=)wHjnJ@&m4R4j3@tS8R~c%t(!44NAC)wv_Nzm0%91PR|mej$&s*)?(*g
z;?`w=w%HUu%DLE9@D*8)u`V*^Y%^Vq`kKy$?cTk6F9^Gxv75rE@z@?erP+pyF1lz9
z;>u%=aXgC&kK2wp!Iv<QFpw~iFpw~iFpw~iFpw~iFpw~iFpw}%mjNrs!Ro?{<_hCW
zeVF^Zme>bk&q}ib^g){4!rbU{jMwGtS*l7<5GD~=EmoO=IRjDY+}G<Fhs)}`4920}
z;Js%N+cKuK_SOnImOm4LrLtXX<uOwc)if??r;s0`e*sxgR#vt!s<r=qz?*zG5g2ST
zH8mPxX|<=@WiW2@pcJ7MFdL!MgJNl{oGJ=2C-gr>_YmI-1m?HY$^=_sewPXSQ}~nj
zb(tP$>epghf^k&msd;&MPmsYnwK@z_$Xc$OIok4Y7?If$N2nWbZAmmZnuRPbB|V=w
z<=%kR29;QtSXPm$7O4`oJuGZ<9@XUus>@fxjyS6LVpm0~K@Zu_Ah1Z%PNQF?*LUL5
z@Q;M#sJ<o*MQ~urJH&6gx0r2`N)H}9cupLKFs#`0k!|jHeO}{S7KlsmyQQ#rO-jzZ
z7n>4HBlK*N&{R$k!x3IGyo^mOeVY1+e{4!H#5K_(aY>NvQ&Uo2#F;bi#71DWt*^5%
zSFX1ZlTk>qs3KFVBGkCI&M+~4PE%fm5sMNmwdBS+aS4#k*h~->3xPqD)^pivY{urB
zv$zOMiv?{pHe>V6St2ms6#5Pz5twfZqYuF5OtcxB@0^Q836`4w^G#L|0oaVqM0#T(
zFy^&CG(lDnVc3k#M0#T(Fo@C)AX|;i*nD%A2+TKyz5_@E=9|Lk1K1KhgnAL!jICbK
zZg0e*1k<Kiw}gcKoXyxwa1jfEX|9jnnl{B+WrV3I&qIprSO_eos%NcI*GVuoVlqlo
zYy_t5{4iH$ls$iem8CHG3&rxcV<RxF9HmNHU`u`oOH*IXDz0NAFeO%JMc|amAF(M-
zVTl+<@X^OgVA{n)$x(ofY~!oq#uUV1N0H<kyN$POdfl$GBAB8X8T|K~F^2ot6Zvq)
zJ5BIn>;$Gf)IlViE3bdYI4|TpEks<}d=eWIt12=rS5N355`KsICl6JJpActc)pJ_<
zL-WRB(?<hr>iDKv+}BwZfpDP)7vZZ~oQ)eRaf$a>R*_1$V{{M+ZL)2tHks*7x$3dr
z9H%yy!>i-QUSJ|Z4gdcA`#;R_N)-rg5SOaXS#n77;HtMehoOiY`G>gIJiOmzUJ0aH
z$Ypj?M+{6vAlD(b^Rr0{?`Xa=IRrS3v_xQOWYqR1$C%e<B>Uh02MSNH#J|B2MF0Q*
M07*qoM6N<$g3q8(AOHXW
--- a/mobile/android/search/res/layout/search_empty.xml
+++ b/mobile/android/search/res/layout/search_empty.xml
@@ -17,36 +17,35 @@
         android:layout_height="0dip"
         android:layout_weight="1"/>
 
     <ImageView
         android:id="@+id/empty_image"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginBottom="15dp"
-        android:src="@drawable/search_fox"
         android:gravity="top|center"
         android:scaleType="fitCenter"/>
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="0dip"
         android:orientation="vertical"
         android:layout_weight="3">
 
         <TextView
+            android:id="@+id/empty_title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="15dp"
-            android:text="@string/search_empty_title"
             style="@style/TextAppearance.EmptyView.Title"
             android:gravity="center"/>
 
         <TextView
+            android:id="@+id/empty_message"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:text="@string/search_empty_message"
             style="@style/TextAppearance.EmptyView.Message"
             android:gravity="center"/>
 
     </LinearLayout>
 
 </LinearLayout>
--- a/mobile/android/search/res/layout/search_fragment_post_search.xml
+++ b/mobile/android/search/res/layout/search_fragment_post_search.xml
@@ -14,10 +14,17 @@
         android:layout_width="match_parent"
         android:layout_height="@dimen/progress_bar_height"
         android:progressDrawable="@drawable/progressbar"/>
 
     <WebView
         android:id="@+id/webview"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
+
+    <ViewStub
+        android:id="@+id/error_view_stub"
+        android:layout="@layout/search_empty"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"/>
+
 </LinearLayout>
-
--- a/mobile/android/search/res/values/search_colors.xml
+++ b/mobile/android/search/res/values/search_colors.xml
@@ -20,9 +20,11 @@
     <color name="widget_text_color">#5F6368</color>
 
     <!--Facet button colors-->
     <color name="facet_button_background_color_default">@android:color/white</color>
     <color name="facet_button_background_color_pressed">#FAFAFA</color>
 
     <color name="facet_button_text_color_default">#ADB0B1</color>
     <color name="facet_button_text_color_selected">#383E42</color>
+
+    <color name="network_error_link">#0092DB</color>
 </resources>
--- a/mobile/android/search/strings/search_strings.dtd
+++ b/mobile/android/search/strings/search_strings.dtd
@@ -18,11 +18,10 @@
 <!ENTITY pref_clearHistory_confirmation 'History cleared'>
 <!ENTITY pref_clearHistory_dialogMessage 'Delete all search history from this device?'>
 <!ENTITY pref_clearHistory_title 'Clear search history'>
 
 <!ENTITY pref_searchProvider_title 'Search engine'>
 
 <!ENTITY search_widget_button_label 'Search'>
 
-<!--  Localization note (default_engine_identifier): Search engine identifier for the default
-      engine. This should be one of the identifiers listed in /searchplugins/list.txt -->
-<!ENTITY default_engine_identifier 'yahoo'>
+<!ENTITY network_error_title 'No internet connection'>
+<!ENTITY network_error_message 'Tap here to check your network settings'>
--- a/mobile/android/search/strings/search_strings.xml.in
+++ b/mobile/android/search/strings/search_strings.xml.in
@@ -13,9 +13,10 @@
     <string name="pref_clearHistory_dialogMessage">&pref_clearHistory_dialogMessage;</string>
     <string name="pref_clearHistory_title">&pref_clearHistory_title;</string>
 
     <string name="pref_searchProvider_title">&pref_searchProvider_title;</string>
 
     <string name="search_widget_name">&search_app_name;</string>
     <string name="search_widget_button_label">&search_widget_button_label;</string>
 
-    <string name="default_engine_identifier">&default_engine_identifier;</string>
+    <string name="network_error_title">&network_error_title;</string>
+    <string name="network_error_message">&network_error_message;</string>
--- a/services/fxaccounts/FxAccountsManager.jsm
+++ b/services/fxaccounts/FxAccountsManager.jsm
@@ -191,30 +191,29 @@ this.FxAccountsManager = {
             return this.getAccount().then(
               (user) => {
                 return this._refreshAuthentication(aAudience, user.email,
                                                    aPrincipal,
                                                    true /* logoutOnFailure */);
               }
             );
           }
-        }
-      );
-
-      // Otherwise, the account was deleted, so ask for Sign In/Up
-      return this._localSignOut().then(
-        () => {
-          return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience,
-                                 aPrincipal);
-        },
-        (reason) => {
-          // reject primary problem, not signout failure
-          log.error("Signing out in response to server error threw: " +
-                    reason);
-          return this._error(reason);
+          // ... otherwise, the account was deleted, so ask for Sign In/Up
+          return this._localSignOut().then(
+            () => {
+              return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience,
+                                     aPrincipal);
+            },
+            (reason) => {
+              // reject primary problem, not signout failure
+              log.error("Signing out in response to server error threw: " +
+                        reason);
+              return this._error(reason);
+            }
+          );
         }
       );
     }
     return Promise.reject(reason);
   },
 
   _getAssertion: function(aAudience, aPrincipal) {
     return this._fxAccounts.getAssertion(aAudience).then(
--- a/storage/public/mozStorageHelper.h
+++ b/storage/public/mozStorageHelper.h
@@ -2,150 +2,178 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MOZSTORAGEHELPER_H
 #define MOZSTORAGEHELPER_H
 
 #include "nsAutoPtr.h"
+#include "nsStringGlue.h"
+#include "mozilla/DebugOnly.h"
 
 #include "mozIStorageAsyncConnection.h"
 #include "mozIStorageConnection.h"
 #include "mozIStorageStatement.h"
+#include "mozIStoragePendingStatement.h"
 #include "nsError.h"
 
 /**
  * This class wraps a transaction inside a given C++ scope, guaranteeing that
  * the transaction will be completed even if you have an exception or
  * return early.
  *
- * aCommitOnComplete controls whether the transaction is committed or rolled
- * back when it goes out of scope. A common use is to create an instance with
- * commitOnComplete = FALSE (rollback), then call Commit on this object manually
- * when your function completes successfully.
+ * A common use is to create an instance with aCommitOnComplete = false (rollback),
+ * then call Commit() on this object manually when your function completes
+ * successfully.
  *
- * Note that nested transactions are not supported by sqlite, so if a transaction
- * is already in progress, this object does nothing. Note that in this case,
- * you may not get the transaction type you ask for, and you won't be able
+ * @note nested transactions are not supported by Sqlite, so if a transaction
+ * is already in progress, this object does nothing.  Note that in this case,
+ * you may not get the transaction type you asked for, and you won't be able
  * to rollback.
  *
- * Note: This class is templatized to be also usable with internal data
- * structures. External users of this class should generally use
- * |mozStorageTransaction| instead.
+ * @param aConnection
+ *        The connection to create the transaction on.
+ * @param aCommitOnComplete
+ *        Controls whether the transaction is committed or rolled back when
+ *        this object goes out of scope.
+ * @param aType [optional]
+ *        The transaction type, as defined in mozIStorageConnection.  Defaults
+ *        to TRANSACTION_DEFERRED.
+ * @param aAsyncCommit [optional]
+ *        Whether commit should be executed asynchronously on the helper thread.
+ *        This is a special option introduced as an interim solution to reduce
+ *        main-thread fsyncs in Places.  Can only be used on main-thread.
+ *
+ *        WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS!
+ *
+ *        Notice that async commit might cause synchronous statements to fail
+ *        with SQLITE_BUSY.  A possible mitigation strategy is to use
+ *        PRAGMA busy_timeout, but notice that might cause main-thread jank.
+ *        Finally, if the database is using WAL journaling mode, other
+ *        connections won't see the changes done in async committed transactions
+ *        until commit is complete.
+ *
+ *        For all of the above reasons, this should only be used as an interim
+ *        solution and avoided completely if possible.
  */
-template<typename T, typename U>
-class mozStorageTransactionBase
+class mozStorageTransaction
 {
 public:
-  mozStorageTransactionBase(T* aConnection,
-                            bool aCommitOnComplete,
-                            int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED)
+  mozStorageTransaction(mozIStorageConnection* aConnection,
+                        bool aCommitOnComplete,
+                        int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED,
+                        bool aAsyncCommit = false)
     : mConnection(aConnection),
       mHasTransaction(false),
       mCommitOnComplete(aCommitOnComplete),
-      mCompleted(false)
+      mCompleted(false),
+      mAsyncCommit(aAsyncCommit)
   {
-    // We won't try to get a transaction if one is already in progress.
-    if (mConnection)
-      mHasTransaction = NS_SUCCEEDED(mConnection->BeginTransactionAs(aType));
+    if (mConnection) {
+      nsAutoCString query("BEGIN");
+      switch(aType) {
+        case mozIStorageConnection::TRANSACTION_IMMEDIATE:
+          query.AppendLiteral(" IMMEDIATE");
+          break;
+        case mozIStorageConnection::TRANSACTION_EXCLUSIVE:
+          query.AppendLiteral(" EXCLUSIVE");
+          break;
+        case mozIStorageConnection::TRANSACTION_DEFERRED:
+          query.AppendLiteral(" DEFERRED");
+          break;
+        default:
+          MOZ_ASSERT(false, "Unknown transaction type");
+      }
+      // If a transaction is already in progress, this will fail, since Sqlite
+      // doesn't support nested transactions.
+      mHasTransaction = NS_SUCCEEDED(mConnection->ExecuteSimpleSQL(query));
+    }
   }
-  ~mozStorageTransactionBase()
+
+  ~mozStorageTransaction()
   {
-    if (mConnection && mHasTransaction && ! mCompleted) {
-      if (mCommitOnComplete)
-        mConnection->CommitTransaction();
-      else
-        mConnection->RollbackTransaction();
+    if (mConnection && mHasTransaction && !mCompleted) {
+      if (mCommitOnComplete) {
+        mozilla::DebugOnly<nsresult> rv = Commit();
+        NS_WARN_IF_FALSE(NS_SUCCEEDED(rv),
+                         "A transaction didn't commit correctly");
+      }
+      else {
+        mozilla::DebugOnly<nsresult> rv = Rollback();
+        NS_WARN_IF_FALSE(NS_SUCCEEDED(rv),
+                         "A transaction didn't rollback correctly");
+      }
     }
   }
 
   /**
    * Commits the transaction if one is in progress. If one is not in progress,
    * this is a NOP since the actual owner of the transaction outside of our
-   * scope is in charge of finally comitting or rolling back the transaction.
+   * scope is in charge of finally committing or rolling back the transaction.
    */
   nsresult Commit()
   {
-    if (!mConnection || mCompleted)
-      return NS_OK; // no connection, or already done
+    if (!mConnection || mCompleted || !mHasTransaction)
+      return NS_OK;
     mCompleted = true;
-    if (! mHasTransaction)
-      return NS_OK; // transaction not ours, ignore
-    nsresult rv = mConnection->CommitTransaction();
+
+    // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle
+    // it, thus the transaction might stay open until the next COMMIT.
+    nsresult rv;
+    if (mAsyncCommit) {
+      nsCOMPtr<mozIStoragePendingStatement> ps;
+      rv = mConnection->ExecuteSimpleSQLAsync(NS_LITERAL_CSTRING("COMMIT"),
+                                              nullptr, getter_AddRefs(ps));
+    }
+    else {
+      rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("COMMIT"));
+    }
+
     if (NS_SUCCEEDED(rv))
       mHasTransaction = false;
 
     return rv;
   }
 
   /**
-   * Rolls back the transaction in progress. You should only call this function
-   * if this object has a real transaction (HasTransaction() = true) because
-   * otherwise, there is no transaction to roll back.
+   * Rolls back the transaction if one is in progress. If one is not in progress,
+   * this is a NOP since the actual owner of the transaction outside of our
+   * scope is in charge of finally rolling back the transaction.
    */
   nsresult Rollback()
   {
-    if (!mConnection || mCompleted)
-      return NS_OK; // no connection, or already done
+    if (!mConnection || mCompleted || !mHasTransaction)
+      return NS_OK;
     mCompleted = true;
-    if (! mHasTransaction)
-      return NS_ERROR_FAILURE;
 
-    // It is possible that a rollback will return busy, so we busy wait...
+    // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return
+    // a busy error, so this handling can be removed.
     nsresult rv = NS_OK;
     do {
-      rv = mConnection->RollbackTransaction();
+      rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK"));
       if (rv == NS_ERROR_STORAGE_BUSY)
         (void)PR_Sleep(PR_INTERVAL_NO_WAIT);
     } while (rv == NS_ERROR_STORAGE_BUSY);
 
     if (NS_SUCCEEDED(rv))
       mHasTransaction = false;
 
     return rv;
   }
 
-  /**
-   * Returns whether this object wraps a real transaction. False means that
-   * this object doesn't do anything because there was already a transaction in
-   * progress when it was created.
-   */
-  bool HasTransaction()
-  {
-    return mHasTransaction;
-  }
-
-  /**
-   * This sets the default action (commit or rollback) when this object goes
-   * out of scope.
-   */
-  void SetDefaultAction(bool aCommitOnComplete)
-  {
-    mCommitOnComplete = aCommitOnComplete;
-  }
-
 protected:
-  U mConnection;
+  nsCOMPtr<mozIStorageConnection> mConnection;
   bool mHasTransaction;
   bool mCommitOnComplete;
   bool mCompleted;
+  bool mAsyncCommit;
 };
 
 /**
- * An instance of the mozStorageTransaction<> family dedicated
- * to |mozIStorageConnection|.
- */
-typedef mozStorageTransactionBase<mozIStorageConnection,
-                                  nsCOMPtr<mozIStorageConnection> >
-mozStorageTransaction;
-
-
-
-/**
  * This class wraps a statement so that it is guaraneed to be reset when
  * this object goes out of scope.
  *
  * Note that this always just resets the statement. If the statement doesn't
  * need resetting, the reset operation is inexpensive.
  */
 class MOZ_STACK_CLASS mozStorageStatementScoper
 {
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -1213,16 +1213,17 @@ Connection::initializeClone(Connection* 
   // Copy over pragmas from the original connection.
   static const char * pragmas[] = {
     "cache_size",
     "temp_store",
     "foreign_keys",
     "journal_size_limit",
     "synchronous",
     "wal_autocheckpoint",
+    "busy_timeout"
   };
   for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) {
     // Read-only connections just need cache_size and temp_store pragmas.
     if (aReadOnly && ::strcmp(pragmas[i], "cache_size") != 0 &&
                      ::strcmp(pragmas[i], "temp_store") != 0) {
       continue;
     }
 
--- a/storage/src/mozStorageConnection.h
+++ b/storage/src/mozStorageConnection.h
@@ -116,16 +116,24 @@ public:
    * @see http://sqlite.org/c3ref/commit_hook.html
    */
   void setCommitHook(int (*aCallbackFn)(void *) , void *aData=nullptr) {
     MOZ_ASSERT(mDBConn, "A connection must exist at this point");
     ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData);
   };
 
   /**
+   * Gets autocommit status.
+   */
+  bool getAutocommit() {
+    MOZ_ASSERT(mDBConn, "A connection must exist at this point");
+    return static_cast<bool>(::sqlite3_get_autocommit(mDBConn));
+  };
+
+  /**
    * Lazily creates and returns a background execution thread.  In the future,
    * the thread may be re-claimed if left idle, so you should call this
    * method just before you dispatch and not save the reference.
    *
    * @returns an event target suitable for asynchronous statement execution.
    */
   nsIEventTarget *getAsyncExecutionTarget();
 
--- a/storage/test/storage_test_harness.h
+++ b/storage/test/storage_test_harness.h
@@ -1,29 +1,36 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "TestHarness.h"
+
 #include "nsMemory.h"
+#include "prthread.h"
 #include "nsThreadUtils.h"
 #include "nsDirectoryServiceDefs.h"
+#include "mozilla/ReentrantMonitor.h"
+
 #include "mozIStorageService.h"
 #include "mozIStorageConnection.h"
 #include "mozIStorageStatementCallback.h"
 #include "mozIStorageCompletionCallback.h"
 #include "mozIStorageBindingParamsArray.h"
 #include "mozIStorageBindingParams.h"
 #include "mozIStorageAsyncStatement.h"
 #include "mozIStorageStatement.h"
 #include "mozIStoragePendingStatement.h"
 #include "mozIStorageError.h"
-#include "nsThreadUtils.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIEventTarget.h"
+
+#include "sqlite3.h"
 
 static int gTotalTests = 0;
 static int gPassedTests = 0;
 
 #define do_check_true(aCondition) \
   PR_BEGIN_MACRO \
     gTotalTests++; \
     if (aCondition) { \
@@ -48,37 +55,37 @@ static int gPassedTests = 0;
 
 #ifdef LINUX
 // XXX Linux opt builds on tinderbox are orange due to linking with stdlib.
 // This is sad and annoying, but it's a workaround that works.
 #define do_check_eq(aExpected, aActual) \
   do_check_true(aExpected == aActual)
 #else
 #include <sstream>
-
 // Print nsresult as uint32_t
 std::ostream& operator<<(std::ostream& aStream, const nsresult aInput)
 {
   return aStream << static_cast<uint32_t>(aInput);
 }
-
 #define do_check_eq(aExpected, aActual) \
   PR_BEGIN_MACRO \
     gTotalTests++; \
     if (aExpected == aActual) { \
       gPassedTests++; \
     } else { \
       std::ostringstream temp; \
       temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \
       temp << aActual <<"' at line " << __LINE__; \
       fail(temp.str().c_str()); \
     } \
   PR_END_MACRO
 #endif
 
+#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK)
+
 already_AddRefed<mozIStorageService>
 getService()
 {
   nsCOMPtr<mozIStorageService> ss =
     do_GetService("@mozilla.org/storage/service;1");
   do_check_true(ss);
   return ss.forget();
 }
@@ -219,8 +226,164 @@ blocking_async_execute(mozIStorageBaseSt
 void
 blocking_async_close(mozIStorageConnection *db)
 {
   nsRefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner());
 
   db->AsyncClose(spinner);
   spinner->SpinUntilCompleted();
 }
+
+////////////////////////////////////////////////////////////////////////////////
+//// Mutex Watching
+
+/**
+ * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on
+ * the caller (generally main) thread.  We do this by decorating the sqlite
+ * mutex logic with our own code that checks what thread it is being invoked on
+ * and sets a flag if it is invoked on the main thread.  We are able to easily
+ * decorate the SQLite mutex logic because SQLite allows us to retrieve the
+ * current function pointers being used and then provide a new set.
+ */
+
+sqlite3_mutex_methods orig_mutex_methods;
+sqlite3_mutex_methods wrapped_mutex_methods;
+
+bool mutex_used_on_watched_thread = false;
+PRThread *watched_thread = nullptr;
+/**
+ * Ugly hack to let us figure out what a connection's async thread is.  If we
+ * were MOZILLA_INTERNAL_API and linked as such we could just include
+ * mozStorageConnection.h and just ask Connection directly.  But that turns out
+ * poorly.
+ *
+ * When the thread a mutex is invoked on isn't watched_thread we save it to this
+ * variable.
+ */
+PRThread *last_non_watched_thread = nullptr;
+
+/**
+ * Set a flag if the mutex is used on the thread we are watching, but always
+ * call the real mutex function.
+ */
+extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex)
+{
+  PRThread *curThread = ::PR_GetCurrentThread();
+  if (curThread == watched_thread)
+    mutex_used_on_watched_thread = true;
+  else
+    last_non_watched_thread = curThread;
+  orig_mutex_methods.xMutexEnter(mutex);
+}
+
+extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex)
+{
+  if (::PR_GetCurrentThread() == watched_thread)
+    mutex_used_on_watched_thread = true;
+  return orig_mutex_methods.xMutexTry(mutex);
+}
+
+void hook_sqlite_mutex()
+{
+  // We need to initialize and teardown SQLite to get it to set up the
+  // default mutex handlers for us so we can steal them and wrap them.
+  do_check_ok(sqlite3_initialize());
+  do_check_ok(sqlite3_shutdown());
+  do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods));
+  do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods));
+  wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter;
+  wrapped_mutex_methods.xMutexTry = wrapped_MutexTry;
+  do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods));
+}
+
+/**
+ * Call to clear the watch state and to set the watching against this thread.
+ *
+ * Check |mutex_used_on_watched_thread| to see if the mutex has fired since
+ * this method was last called.  Since we're talking about the current thread,
+ * there are no race issues to be concerned about
+ */
+void watch_for_mutex_use_on_this_thread()
+{
+  watched_thread = ::PR_GetCurrentThread();
+  mutex_used_on_watched_thread = false;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Thread Wedgers
+
+/**
+ * A runnable that blocks until code on another thread invokes its unwedge
+ * method.  By dispatching this to a thread you can ensure that no subsequent
+ * runnables dispatched to the thread will execute until you invoke unwedge.
+ *
+ * The wedger is self-dispatching, just construct it with its target.
+ */
+class ThreadWedger : public nsRunnable
+{
+public:
+  explicit ThreadWedger(nsIEventTarget *aTarget)
+  : mReentrantMonitor("thread wedger")
+  , unwedged(false)
+  {
+    aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL);
+  }
+
+  NS_IMETHOD Run()
+  {
+    mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor);
+
+    if (!unwedged)
+      automon.Wait();
+
+    return NS_OK;
+  }
+
+  void unwedge()
+  {
+    mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor);
+    unwedged = true;
+    automon.Notify();
+  }
+
+private:
+  mozilla::ReentrantMonitor mReentrantMonitor;
+  bool unwedged;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Async Helpers
+
+/**
+ * A horrible hack to figure out what the connection's async thread is.  By
+ * creating a statement and async dispatching we can tell from the mutex who
+ * is the async thread, PRThread style.  Then we map that to an nsIThread.
+ */
+already_AddRefed<nsIThread>
+get_conn_async_thread(mozIStorageConnection *db)
+{
+  // Make sure we are tracking the current thread as the watched thread
+  watch_for_mutex_use_on_this_thread();
+
+  // - statement with nothing to bind
+  nsCOMPtr<mozIStorageAsyncStatement> stmt;
+  db->CreateAsyncStatement(
+    NS_LITERAL_CSTRING("SELECT 1"),
+    getter_AddRefs(stmt));
+  blocking_async_execute(stmt);
+  stmt->Finalize();
+
+  nsCOMPtr<nsIThreadManager> threadMan =
+    do_GetService("@mozilla.org/thread-manager;1");
+  nsCOMPtr<nsIThread> asyncThread;
+  threadMan->GetThreadFromPRThread(last_non_watched_thread,
+                                   getter_AddRefs(asyncThread));
+
+  // Additionally, check that the thread we get as the background thread is the
+  // same one as the one we report from getInterface.
+  nsCOMPtr<nsIEventTarget> target = do_GetInterface(db);
+  nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target);
+  PRThread *allegedPRThread;
+  (void)allegedAsyncThread->GetPRThread(&allegedPRThread);
+  do_check_eq(allegedPRThread, last_non_watched_thread);
+  return asyncThread.forget();
+}
--- a/storage/test/test_transaction_helper.cpp
+++ b/storage/test/test_transaction_helper.cpp
@@ -2,181 +2,174 @@
  * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "storage_test_harness.h"
 
 #include "mozStorageHelper.h"
+#include "mozStorageConnection.h"
+
+using namespace mozilla;
+using namespace mozilla::storage;
+
+bool has_transaction(mozIStorageConnection* aDB) {
+  return !(static_cast<Connection *>(aDB)->getAutocommit());
+}
 
 /**
  * This file test our Transaction helper in mozStorageHelper.h.
  */
 
 void
-test_HasTransaction()
-{
-  nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
-
-  // First test that it holds the transaction after it should have gotten one.
-  {
-    mozStorageTransaction transaction(db, false);
-    do_check_true(transaction.HasTransaction());
-    (void)transaction.Commit();
-    // And that it does not have a transaction after we have committed.
-    do_check_false(transaction.HasTransaction());
-  }
-
-  // Check that no transaction is had after a rollback.
-  {
-    mozStorageTransaction transaction(db, false);
-    do_check_true(transaction.HasTransaction());
-    (void)transaction.Rollback();
-    do_check_false(transaction.HasTransaction());
-  }
-
-  // Check that we do not have a transaction if one is already obtained.
-  mozStorageTransaction outerTransaction(db, false);
-  do_check_true(outerTransaction.HasTransaction());
-  {
-    mozStorageTransaction innerTransaction(db, false);
-    do_check_false(innerTransaction.HasTransaction());
-  }
-}
-
-void
 test_Commit()
 {
   nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
 
   // Create a table in a transaction, call Commit, and make sure that it does
   // exists after the transaction falls out of scope.
   {
     mozStorageTransaction transaction(db, false);
+    do_check_true(has_transaction(db));
     (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
       "CREATE TABLE test (id INTEGER PRIMARY KEY)"
     ));
     (void)transaction.Commit();
   }
+  do_check_false(has_transaction(db));
 
   bool exists = false;
   (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
   do_check_true(exists);
 }
 
 void
 test_Rollback()
 {
   nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
 
   // Create a table in a transaction, call Rollback, and make sure that it does
   // not exists after the transaction falls out of scope.
   {
     mozStorageTransaction transaction(db, true);
+    do_check_true(has_transaction(db));
     (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
       "CREATE TABLE test (id INTEGER PRIMARY KEY)"
     ));
     (void)transaction.Rollback();
   }
+  do_check_false(has_transaction(db));
 
   bool exists = true;
   (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
   do_check_false(exists);
 }
 
 void
 test_AutoCommit()
 {
   nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
 
   // Create a table in a transaction, and make sure that it exists after the
   // transaction falls out of scope.  This means the Commit was successful.
   {
     mozStorageTransaction transaction(db, true);
+    do_check_true(has_transaction(db));
     (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
       "CREATE TABLE test (id INTEGER PRIMARY KEY)"
     ));
   }
+  do_check_false(has_transaction(db));
 
   bool exists = false;
   (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
   do_check_true(exists);
 }
 
 void
 test_AutoRollback()
 {
   nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
 
   // Create a table in a transaction, and make sure that it does not exists
   // after the transaction falls out of scope.  This means the Rollback was
   // successful.
   {
     mozStorageTransaction transaction(db, false);
+    do_check_true(has_transaction(db));
     (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
       "CREATE TABLE test (id INTEGER PRIMARY KEY)"
     ));
   }
+  do_check_false(has_transaction(db));
 
   bool exists = true;
   (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
   do_check_false(exists);
 }
 
 void
-test_SetDefaultAction()
-{
-  nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
-
-  // First we test that rollback happens when we first set it to automatically
-  // commit.
-  {
-    mozStorageTransaction transaction(db, true);
-    (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-      "CREATE TABLE test1 (id INTEGER PRIMARY KEY)"
-    ));
-    transaction.SetDefaultAction(false);
-  }
-  bool exists = true;
-  (void)db->TableExists(NS_LITERAL_CSTRING("test1"), &exists);
-  do_check_false(exists);
-
-  // Now we do the opposite and test that a commit happens when we first set it
-  // to automatically rollback.
-  {
-    mozStorageTransaction transaction(db, false);
-    (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-      "CREATE TABLE test2 (id INTEGER PRIMARY KEY)"
-    ));
-    transaction.SetDefaultAction(true);
-  }
-  exists = false;
-  (void)db->TableExists(NS_LITERAL_CSTRING("test2"), &exists);
-  do_check_true(exists);
-}
-
-void
 test_null_database_connection()
 {
   // We permit the use of the Transaction helper when passing a null database
   // in, so we need to make sure this still works without crashing.
   mozStorageTransaction transaction(nullptr, false);
-
-  do_check_false(transaction.HasTransaction());
   do_check_true(NS_SUCCEEDED(transaction.Commit()));
   do_check_true(NS_SUCCEEDED(transaction.Rollback()));
 }
 
+void
+test_async_Commit()
+{
+  // note this will be active for any following test.
+  hook_sqlite_mutex();
+
+  nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+  // -- wedge the thread
+  nsCOMPtr<nsIThread> target(get_conn_async_thread(db));
+  do_check_true(target);
+  nsRefPtr<ThreadWedger> wedger (new ThreadWedger(target));
+
+  {
+    mozStorageTransaction transaction(db, false,
+                                      mozIStorageConnection::TRANSACTION_DEFERRED,
+                                      true);
+    do_check_true(has_transaction(db));
+    (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+    ));
+    (void)transaction.Commit();
+  }
+  do_check_true(has_transaction(db));
+
+  // -- unwedge the async thread
+  wedger->unwedge();
+
+  // Ensure the transaction has done its job by enqueueing an async execution.
+  nsCOMPtr<mozIStorageAsyncStatement> stmt;
+  (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "SELECT NULL"
+  ), getter_AddRefs(stmt));
+  blocking_async_execute(stmt);
+  stmt->Finalize();
+  do_check_false(has_transaction(db));
+  bool exists = false;
+  (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
+  do_check_true(exists);
+
+  blocking_async_close(db);
+}
+
 void (*gTests[])(void) = {
-  test_HasTransaction,
   test_Commit,
   test_Rollback,
   test_AutoCommit,
   test_AutoRollback,
-  test_SetDefaultAction,
   test_null_database_connection,
+  test_async_Commit,
 };
 
 const char *file = __FILE__;
 #define TEST_NAME "transaction helper"
 #define TEST_FILE file
 #include "storage_test_harness_tail.h"
--- a/storage/test/test_true_async.cpp
+++ b/storage/test/test_true_async.cpp
@@ -1,184 +1,15 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "storage_test_harness.h"
-#include "prthread.h"
-#include "nsIEventTarget.h"
-#include "nsIInterfaceRequestorUtils.h"
-
-#include "sqlite3.h"
-
-#include "mozilla/ReentrantMonitor.h"
-
-using mozilla::ReentrantMonitor;
-using mozilla::ReentrantMonitorAutoEnter;
-
-/**
- * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on
- * the caller (generally main) thread.  We do this by decorating the sqlite
- * mutex logic with our own code that checks what thread it is being invoked on
- * and sets a flag if it is invoked on the main thread.  We are able to easily
- * decorate the SQLite mutex logic because SQLite allows us to retrieve the
- * current function pointers being used and then provide a new set.
- */
-
-/* ===== Mutex Watching ===== */
-
-sqlite3_mutex_methods orig_mutex_methods;
-sqlite3_mutex_methods wrapped_mutex_methods;
-
-bool mutex_used_on_watched_thread = false;
-PRThread *watched_thread = nullptr;
-/**
- * Ugly hack to let us figure out what a connection's async thread is.  If we
- * were MOZILLA_INTERNAL_API and linked as such we could just include
- * mozStorageConnection.h and just ask Connection directly.  But that turns out
- * poorly.
- *
- * When the thread a mutex is invoked on isn't watched_thread we save it to this
- * variable.
- */
-PRThread *last_non_watched_thread = nullptr;
-
-/**
- * Set a flag if the mutex is used on the thread we are watching, but always
- * call the real mutex function.
- */
-extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex)
-{
-  PRThread *curThread = ::PR_GetCurrentThread();
-  if (curThread == watched_thread)
-    mutex_used_on_watched_thread = true;
-  else
-    last_non_watched_thread = curThread;
-  orig_mutex_methods.xMutexEnter(mutex);
-}
-
-extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex)
-{
-  if (::PR_GetCurrentThread() == watched_thread)
-    mutex_used_on_watched_thread = true;
-  return orig_mutex_methods.xMutexTry(mutex);
-}
-
-
-#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK)
-
-void hook_sqlite_mutex()
-{
-  // We need to initialize and teardown SQLite to get it to set up the
-  // default mutex handlers for us so we can steal them and wrap them.
-  do_check_ok(sqlite3_initialize());
-  do_check_ok(sqlite3_shutdown());
-  do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods));
-  do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods));
-  wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter;
-  wrapped_mutex_methods.xMutexTry = wrapped_MutexTry;
-  do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods));
-}
-
-/**
- * Call to clear the watch state and to set the watching against this thread.
- *
- * Check |mutex_used_on_watched_thread| to see if the mutex has fired since
- * this method was last called.  Since we're talking about the current thread,
- * there are no race issues to be concerned about
- */
-void watch_for_mutex_use_on_this_thread()
-{
-  watched_thread = ::PR_GetCurrentThread();
-  mutex_used_on_watched_thread = false;
-}
-
-
-////////////////////////////////////////////////////////////////////////////////
-//// Thread Wedgers
-
-/**
- * A runnable that blocks until code on another thread invokes its unwedge
- * method.  By dispatching this to a thread you can ensure that no subsequent
- * runnables dispatched to the thread will execute until you invoke unwedge.
- *
- * The wedger is self-dispatching, just construct it with its target.
- */
-class ThreadWedger : public nsRunnable
-{
-public:
-  explicit ThreadWedger(nsIEventTarget *aTarget)
-  : mReentrantMonitor("thread wedger")
-  , unwedged(false)
-  {
-    aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL);
-  }
-
-  NS_IMETHOD Run()
-  {
-    ReentrantMonitorAutoEnter automon(mReentrantMonitor);
-
-    if (!unwedged)
-      automon.Wait();
-
-    return NS_OK;
-  }
-
-  void unwedge()
-  {
-    ReentrantMonitorAutoEnter automon(mReentrantMonitor);
-    unwedged = true;
-    automon.Notify();
-  }
-
-private:
-  ReentrantMonitor mReentrantMonitor;
-  bool unwedged;
-};
-
-////////////////////////////////////////////////////////////////////////////////
-//// Async Helpers
-
-/**
- * A horrible hack to figure out what the connection's async thread is.  By
- * creating a statement and async dispatching we can tell from the mutex who
- * is the async thread, PRThread style.  Then we map that to an nsIThread.
- */
-already_AddRefed<nsIThread>
-get_conn_async_thread(mozIStorageConnection *db)
-{
-  // Make sure we are tracking the current thread as the watched thread
-  watch_for_mutex_use_on_this_thread();
-
-  // - statement with nothing to bind
-  nsCOMPtr<mozIStorageAsyncStatement> stmt;
-  db->CreateAsyncStatement(
-    NS_LITERAL_CSTRING("SELECT 1"),
-    getter_AddRefs(stmt));
-  blocking_async_execute(stmt);
-  stmt->Finalize();
-
-  nsCOMPtr<nsIThreadManager> threadMan =
-    do_GetService("@mozilla.org/thread-manager;1");
-  nsCOMPtr<nsIThread> asyncThread;
-  threadMan->GetThreadFromPRThread(last_non_watched_thread,
-                                   getter_AddRefs(asyncThread));
-
-  // Additionally, check that the thread we get as the background thread is the
-  // same one as the one we report from getInterface.
-  nsCOMPtr<nsIEventTarget> target = do_GetInterface(db);
-  nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target);
-  PRThread *allegedPRThread;
-  (void)allegedAsyncThread->GetPRThread(&allegedPRThread);
-  do_check_eq(allegedPRThread, last_non_watched_thread);
-  return asyncThread.forget();
-}
-
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Tests
 
 void
 test_TrueAsyncStatement()
 {
   // (only the first test needs to call this)
--- a/storage/test/unit/test_storage_connection.js
+++ b/storage/test/unit/test_storage_connection.js
@@ -731,16 +731,17 @@ add_task(function test_clone_copies_prag
 {
   const PRAGMAS = [
     { name: "cache_size", value: 500, copied: true },
     { name: "temp_store", value: 2, copied: true },
     { name: "foreign_keys", value: 1, copied: true },
     { name: "journal_size_limit", value: 524288, copied: true },
     { name: "synchronous", value: 2, copied: true },
     { name: "wal_autocheckpoint", value: 16, copied: true },
+    { name: "busy_timeout", value: 50, copied: true },
     { name: "ignore_check_constraints", value: 1, copied: false },
   ];
 
   let db1 = getService().openUnsharedDatabase(getTestDB());
 
   // Sanity check initial values are different from enforced ones.
   PRAGMAS.forEach(function (pragma) {
     let stmt = db1.createStatement("PRAGMA " + pragma.name);
@@ -773,16 +774,17 @@ add_task(function test_readonly_clone_co
 {
   const PRAGMAS = [
     { name: "cache_size", value: 500, copied: true },
     { name: "temp_store", value: 2, copied: true },
     { name: "foreign_keys", value: 1, copied: false },
     { name: "journal_size_limit", value: 524288, copied: false },
     { name: "synchronous", value: 2, copied: false },
     { name: "wal_autocheckpoint", value: 16, copied: false },
+    { name: "busy_timeout", value: 50, copied: false },
     { name: "ignore_check_constraints", value: 1, copied: false },
   ];
 
   let db1 = getService().openUnsharedDatabase(getTestDB());
 
   // Sanity check initial values are different from enforced ones.
   PRAGMAS.forEach(function (pragma) {
     let stmt = db1.createStatement("PRAGMA " + pragma.name);
--- a/toolkit/components/parentalcontrols/nsIParentalControlsService.idl
+++ b/toolkit/components/parentalcontrols/nsIParentalControlsService.idl
@@ -6,32 +6,53 @@
 
 #include "nsISupports.idl"
 
 interface nsIURI;
 interface nsIFile;
 interface nsIInterfaceRequestor;
 interface nsIArray;
 
-[scriptable, uuid(871cf229-2b21-4f04-b24d-e08061f14815)]
+[scriptable, uuid(b3585b2a-b4b3-4aa7-be92-b8ddaa6aec5f)]
 interface nsIParentalControlsService : nsISupports
 {
   /**
+   * Action types that can be blocked for users.
+   */
+  const short DOWNLOAD = 1; // Downloading files
+  const short INSTALL_EXTENSION = 2; // Installing extensions
+  const short INSTALL_APP = 3; // Installing webapps
+  const short VISIT_FILE_URLS = 4; // Opening file:/// urls
+  const short SHARE = 5; // Sharing
+  const short BOOKMARK = 6; // Creating bookmarks
+  const short ADD_CONTACT = 7; // Add contacts to the system database
+  const short SET_IMAGE = 8; // Setting images as wall paper
+
+  /**
    * @returns true if the current user account has parental controls
    * restrictions enabled.
    */ 
   readonly attribute boolean parentalControlsEnabled;
 
   /**
    * @returns true if the current user account parental controls
    * restrictions include the blocking of all file downloads.
    */ 
   readonly attribute boolean blockFileDownloadsEnabled;
 
   /**
+   * Check if the user can do the prescibed action for this uri.
+   *
+   * @param aAction             Action being performed
+   * @param aUri                The uri requesting this action
+   * @param aWindow             The window generating this event.
+   */
+   boolean isAllowed(in short aAction, [optional] in nsIURI aUri);
+
+  /**
    * Request that blocked URI(s) be allowed through parental
    * control filters. Returns true if the URI was successfully
    * overriden. Note, may block while native UI is shown.
    *
    * @param aTarget(s)          URI to be overridden. In the case of
    *                            multiple URI, the first URI in the array
    *                            should be the root URI of the site.
    * @param window              Window that generates the event.
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp
@@ -9,17 +9,17 @@
 #include "nsIFile.h"
 
 NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService)
 
 nsParentalControlsService::nsParentalControlsService() :
   mEnabled(false)
 {
   if (mozilla::AndroidBridge::HasEnv()) {
-    mEnabled = mozilla::widget::android::GeckoAppShell::IsUserRestricted();
+    mEnabled = mozilla::widget::android::RestrictedProfiles::IsUserRestricted();
   }
 }
 
 nsParentalControlsService::~nsParentalControlsService()
 {
 }
 
 NS_IMETHODIMP
@@ -27,17 +27,21 @@ nsParentalControlsService::GetParentalCo
 {
   *aResult = mEnabled;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult)
 {
-  return NS_ERROR_NOT_AVAILABLE;
+  bool res;
+  IsAllowed(nsIParentalControlsService::DOWNLOAD, NULL, &res);
+  *aResult = res;
+
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 nsParentalControlsService::GetLoggingEnabled(bool *aResult)
 {
   return NS_ERROR_NOT_AVAILABLE;
 }
 
@@ -60,8 +64,35 @@ nsParentalControlsService::RequestURIOve
 
 NS_IMETHODIMP
 nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
                                                nsIInterfaceRequestor *aWindowContext,
                                                bool *_retval)
 {
   return NS_ERROR_NOT_AVAILABLE;
 }
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+                                     nsIURI *aUri,
+                                     bool *_retval)
+{
+  nsresult rv = NS_OK;
+  *_retval = true;
+
+  if (!mEnabled) {
+    return rv;
+  }
+
+  if (mozilla::AndroidBridge::HasEnv()) {
+    nsAutoCString url;
+    if (aUri) {
+      rv = aUri->GetSpec(url);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+
+    *_retval = mozilla::widget::android::RestrictedProfiles::IsAllowed(aAction,
+                                                    NS_ConvertUTF8toUTF16(url));
+    return rv;
+  }
+
+  return NS_ERROR_NOT_AVAILABLE;
+}
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm
@@ -60,8 +60,17 @@ nsParentalControlsService::RequestURIOve
 
 NS_IMETHODIMP
 nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
                                                nsIInterfaceRequestor *aWindowContext,
                                                bool *_retval)
 {
   return NS_ERROR_NOT_AVAILABLE;
 }
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+                                     nsIURI *aUri,
+                                     bool *_retval)
+{
+  return NS_ERROR_NOT_AVAILABLE;
+}
+
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp
@@ -55,8 +55,16 @@ nsParentalControlsService::RequestURIOve
 
 NS_IMETHODIMP
 nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
                                                nsIInterfaceRequestor *aWindowContext,
                                                bool *_retval)
 {
   return NS_ERROR_NOT_AVAILABLE;
 }
+
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+                                     nsIURI *aUri,
+                                     bool *_retval)
+{
+  return NS_ERROR_NOT_AVAILABLE;
+}
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp
+++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp
@@ -326,8 +326,15 @@ nsParentalControlsService::LogFileDownlo
   }
   else {
     EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_PATH], (const void*)fill, sizeof(fill));
   }
 
   gEventWrite(mProvider, &WPCEVENT_WEB_FILEDOWNLOAD, ARRAYSIZE(eventData), eventData);
 }
 
+NS_IMETHODIMP
+nsParentalControlsService::IsAllowed(int16_t aAction,
+                                     nsIURI *aUri,
+                                     bool *_retval)
+{
+  return NS_ERROR_NOT_AVAILABLE;
+}
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -44,16 +44,19 @@
 
 // Maximum size for the WAL file.  It should be small enough since in case of
 // crashes we could lose all the transactions in the file.  But a too small
 // file could hurt performance.
 #define DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES 512
 
 #define BYTES_PER_KIBIBYTE 1024
 
+// How much time Sqlite can wait before returning a SQLITE_BUSY error.
+#define DATABASE_BUSY_TIMEOUT_MS 100
+
 // Old Sync GUID annotation.
 #define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid")
 
 // Places string bundle, contains internationalized bookmark root names.
 #define PLACES_BUNDLE "chrome://places/locale/places.properties"
 
 // Livemarks annotations.
 #define LMANNO_FEEDURI "livemark/feedURI"
@@ -592,16 +595,20 @@ Database::InitSchema(bool* aDatabaseMigr
   // Grow places in |growthIncrementKiB| increments to limit fragmentation on disk.
   // By default, it's 10 MB.
   int32_t growthIncrementKiB =
     Preferences::GetInt(PREF_GROWTH_INCREMENT_KIB, 10 * BYTES_PER_KIBIBYTE);
   if (growthIncrementKiB > 0) {
     (void)mMainConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE, EmptyCString());
   }
 
+  nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = ");
+  busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS);
+  (void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma);
+
   // We use our functions during migration, so initialize them now.
   rv = InitFunctions();
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Get the database schema version.
   int32_t currentSchemaVersion;
   rv = mMainConn->GetSchemaVersion(&currentSchemaVersion);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -1205,18 +1212,16 @@ Database::MigrateV7Up()
   nsresult rv = mMainConn->IndexExists(NS_LITERAL_CSTRING(
     "moz_places_url_uniqueindex"
   ), &URLUniqueIndexExists);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!URLUniqueIndexExists) {
     return NS_ERROR_FILE_CORRUPTED;
   }
 
-  mozStorageTransaction transaction(mMainConn, false);
-
   // We need an index on lastModified to catch quickly last modified bookmark
   // title for tag container's children. This will be useful for Sync, too.
   bool lastModIndexExists = false;
   rv = mMainConn->IndexExists(
     NS_LITERAL_CSTRING("moz_bookmarks_itemlastmodifiedindex"),
     &lastModIndexExists);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -1386,25 +1391,24 @@ Database::MigrateV7Up()
   rv = mMainConn->TableExists(NS_LITERAL_CSTRING("moz_inputhistory"),
                               &tableExists);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!tableExists) {
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_INPUTHISTORY);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  return transaction.Commit();
+  return NS_OK;
 }
 
 
 nsresult
 Database::MigrateV8Up()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  mozStorageTransaction transaction(mMainConn, false);
 
   nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
       "DROP TRIGGER IF EXISTS moz_historyvisits_afterinsert_v1_trigger"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
       "DROP TRIGGER IF EXISTS moz_historyvisits_afterdelete_v1_trigger"));
   NS_ENSURE_SUCCESS(rv, rv);
@@ -1440,25 +1444,24 @@ Database::MigrateV8Up()
         "DROP INDEX IF EXISTS moz_items_annos_attributesindex"));
     NS_ENSURE_SUCCESS(rv, rv);
 
     // create new item annos index
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ITEMSANNOS_PLACEATTRIBUTE);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  return transaction.Commit();
+  return NS_OK;
 }
 
 
 nsresult
 Database::MigrateV9Up()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  mozStorageTransaction transaction(mMainConn, false);
   // Added in Bug 488966.  The last_visit_date column caches the last
   // visit date, this enhances SELECT performances when we
   // need to sort visits by visit date.
   // The cached value is synced by triggers on every added or removed visit.
   // See nsPlacesTriggers.h for details on the triggers.
   bool oldIndexExists = false;
   nsresult rv = mMainConn->IndexExists(
     NS_LITERAL_CSTRING("moz_places_lastvisitdateindex"), &oldIndexExists);
@@ -1480,17 +1483,17 @@ Database::MigrateV9Up()
     rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
         "UPDATE moz_places SET last_visit_date = "
           "(SELECT MAX(visit_date) "
            "FROM moz_historyvisits "
            "WHERE place_id = moz_places.id)"));
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  return transaction.Commit();
+  return NS_OK;
 }
 
 
 nsresult
 Database::MigrateV10Up()
 {
   MOZ_ASSERT(NS_IsMainThread());
   // LastModified is set to the same value as dateAdded on item creation.
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -2262,17 +2262,19 @@ nsNavHistory::RemoveObserver(nsINavHisto
 }
 
 // nsNavHistory::BeginUpdateBatch
 // See RunInBatchMode
 nsresult
 nsNavHistory::BeginUpdateBatch()
 {
   if (mBatchLevel++ == 0) {
-    mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false);
+    mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false,
+                                                    mozIStorageConnection::TRANSACTION_DEFERRED,
+                                                    true);
 
     NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                      nsINavHistoryObserver, OnBeginUpdateBatch());
   }
   return NS_OK;
 }
 
 // nsNavHistory::EndUpdateBatch
@@ -2327,17 +2329,19 @@ nsNavHistory::GetHistoryDisabled(bool *_
 
 nsresult
 nsNavHistory::RemovePagesInternal(const nsCString& aPlaceIdsQueryString)
 {
   // Return early if there is nothing to delete.
   if (aPlaceIdsQueryString.IsEmpty())
     return NS_OK;
 
-  mozStorageTransaction transaction(mDB->MainConn(), false);
+  mozStorageTransaction transaction(mDB->MainConn(), false,
+                                    mozIStorageConnection::TRANSACTION_DEFERRED,
+                                    true);
 
   // Delete all visits for the specified place ids.
   nsresult rv = mDB->MainConn()->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING(
       "DELETE FROM moz_historyvisits WHERE place_id IN (") +
         aPlaceIdsQueryString +
         NS_LITERAL_CSTRING(")")
   );
@@ -2719,17 +2723,19 @@ nsNavHistory::RemoveVisitsByTimeframe(PR
         deletePlaceIdsQueryString.AppendInt(placeId);
       }
     }
   }
 
   // force a full refresh calling onEndUpdateBatch (will call Refresh())
   UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers
 
-  mozStorageTransaction transaction(mDB->MainConn(), false);
+  mozStorageTransaction transaction(mDB->MainConn(), false,
+                                    mozIStorageConnection::TRANSACTION_DEFERRED,
+                                    true);
 
   // Delete all visits within the timeframe.
   nsCOMPtr<mozIStorageStatement> deleteVisitsStmt = mDB->GetStatement(
     "DELETE FROM moz_historyvisits "
     "WHERE :from_date <= visit_date AND visit_date <= :to_date"
   );
   NS_ENSURE_STATE(deleteVisitsStmt);
   mozStorageStatementScoper deletevisitsScoper(deleteVisitsStmt);
--- a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js
+++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js
@@ -6,17 +6,18 @@
 
 /* Bug 1017502 - Add a foreign_count column to moz_places
 This tests, tests the triggers that adjust the foreign_count when a bookmark is
 added or removed and also the maintenance task to fix wrong counts.
 */
 
 const T_URI = NetUtil.newURI("https://www.mozilla.org/firefox/nightly/firstrun/");
 
-function* getForeignCountForURL(conn, url){
+function* getForeignCountForURL(conn, url) {
+  yield promiseAsyncUpdates();
   let url = url instanceof Ci.nsIURI ? url.spec : url;
   let rows = yield conn.executeCached(
       "SELECT foreign_count FROM moz_places WHERE url = :t_url ", { t_url: url });
   return rows[0].getResultByName("foreign_count");
 }
 
 function run_test() {
   run_next_test();
@@ -101,9 +102,9 @@ add_task(function* add_remove_tags_test(
 
   // Check foreign count is incremented by 2 for two tags
   PlacesUtils.tagging.tagURI(T_URI, ["one", "two"]);
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 3);
 
   // Check foreign count is set to 0 when all tags are removed
   PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]);
   Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0);
-});
\ No newline at end of file
+});
--- a/toolkit/devtools/server/actors/timeline.js
+++ b/toolkit/devtools/server/actors/timeline.js
@@ -23,17 +23,17 @@
  */
 
 const {Ci, Cu} = require("chrome");
 const protocol = require("devtools/server/protocol");
 const {method, Arg, RetVal} = protocol;
 const events = require("sdk/event/core");
 const {setTimeout, clearTimeout} = require("sdk/timers");
 
-const TIMELINE_DATA_PULL_TIMEOUT = 300;
+const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms
 
 exports.register = function(handle) {
   handle.addGlobalActor(TimelineActor, "timelineActor");
   handle.addTabActor(TimelineActor, "timelineActor");
 };
 
 exports.unregister = function(handle) {
   handle.removeGlobalActor(TimelineActor);
@@ -85,18 +85,19 @@ let TimelineActor = protocol.ActorClass(
    * At regular intervals, pop the markers from the docshell, and forward
    * markers if any.
    */
   _pullTimelineData: function() {
     let markers = this.docshell.popProfileTimelineMarkers();
     if (markers.length > 0) {
       events.emit(this, "markers", markers);
     }
-    this._dataPullTimeout = setTimeout(() => this._pullTimelineData(),
-                                       TIMELINE_DATA_PULL_TIMEOUT);
+    this._dataPullTimeout = setTimeout(() => {
+      this._pullTimelineData();
+    }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT);
   },
 
   /**
    * Are we recording profile markers for the current docshell (window)?
    */
   isRecording: method(function() {
     return this.docshell.recordProfileTimelineMarkers;
   }, {
@@ -109,24 +110,24 @@ let TimelineActor = protocol.ActorClass(
   /**
    * Start/stop recording profile markers.
    */
   start: method(function() {
     if (!this.docshell.recordProfileTimelineMarkers) {
       this.docshell.recordProfileTimelineMarkers = true;
       this._pullTimelineData();
     }
-  }, {oneway: true}),
+  }, {}),
 
   stop: method(function() {
     if (this.docshell.recordProfileTimelineMarkers) {
       this.docshell.recordProfileTimelineMarkers = false;
       clearTimeout(this._dataPullTimeout);
     }
-  }, {oneway: true}),
+  }, {}),
 });
 
 exports.TimelineFront = protocol.FrontClass(TimelineActor, {
   initialize: function(client, {timelineActor}) {
     protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
     this.manage(this);
   },
 
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -1067,29 +1067,52 @@ this.AddonRepository = {
           addon[INTEGER_KEY_MAP[localName]] = value;
         continue;
       }
 
       // Handle cases that aren't as simple as grabbing the text content
       switch (localName) {
         case "type":
           // Map AMO's type id to corresponding string
+          // https://github.com/mozilla/olympia/blob/master/apps/constants/base.py#L127
+          // These definitions need to be updated whenever AMO adds a new type.
           let id = parseInt(node.getAttribute("id"));
           switch (id) {
             case 1:
               addon.type = "extension";
               break;
             case 2:
               addon.type = "theme";
               break;
             case 3:
               addon.type = "dictionary";
               break;
+            case 4:
+              addon.type = "search";
+              break;
+            case 5:
+              addon.type = "langpack";
+              break;
+            case 6:
+              addon.type = "langpack-addon";
+              break;
+            case 7:
+              addon.type = "plugin";
+              break;
+            case 8:
+              addon.type = "api";
+              break;
+            case 9:
+              addon.type = "lightweight-theme";
+              break;
+            case 11:
+              addon.type = "webapp";
+              break;
             default:
-              logger.warn("Unknown type id when parsing addon: " + id);
+              logger.info("Unknown type id " + id + " found when parsing response for GUID " + guid);
           }
           break;
         case "authors":
           let authorNodes = node.getElementsByTagName("author");
           for (let authorNode of authorNodes) {
             let name = self._getDescendantTextContent(authorNode, "name");
             let link = self._getDescendantTextContent(authorNode, "link");
             if (name == null || link == null)
--- a/widget/android/GeneratedJNIWrappers.cpp
+++ b/widget/android/GeneratedJNIWrappers.cpp
@@ -50,25 +50,23 @@ jmethodID GeckoAppShell::jGetIconForExte
 jmethodID GeckoAppShell::jGetMessageWrapper = 0;
 jmethodID GeckoAppShell::jGetMimeTypeFromExtensionsWrapper = 0;
 jmethodID GeckoAppShell::jGetNextMessageInListWrapper = 0;
 jmethodID GeckoAppShell::jGetProxyForURIWrapper = 0;
 jmethodID GeckoAppShell::jGetScreenDepthWrapper = 0;
 jmethodID GeckoAppShell::jGetScreenOrientationWrapper = 0;
 jmethodID GeckoAppShell::jGetShowPasswordSetting = 0;
 jmethodID GeckoAppShell::jGetSystemColoursWrapper = 0;
-jmethodID GeckoAppShell::jGetUserRestrictions = 0;
 jmethodID GeckoAppShell::jHandleGeckoMessageWrapper = 0;
 jmethodID GeckoAppShell::jHandleUncaughtException = 0;
 jmethodID GeckoAppShell::jHideProgressDialog = 0;
 jmethodID GeckoAppShell::jInitCameraWrapper = 0;
 jmethodID GeckoAppShell::jIsNetworkLinkKnown = 0;
 jmethodID GeckoAppShell::jIsNetworkLinkUp = 0;
 jmethodID GeckoAppShell::jIsTablet = 0;
-jmethodID GeckoAppShell::jIsUserRestricted = 0;
 jmethodID GeckoAppShell::jKillAnyZombies = 0;
 jmethodID GeckoAppShell::jLoadPluginClass = 0;
 jmethodID GeckoAppShell::jLockScreenOrientation = 0;
 jmethodID GeckoAppShell::jMarkURIVisited = 0;
 jmethodID GeckoAppShell::jMoveTaskToBack = 0;
 jmethodID GeckoAppShell::jNetworkLinkType = 0;
 jmethodID GeckoAppShell::jNotifyDefaultPrevented = 0;
 jmethodID GeckoAppShell::jNotifyIME = 0;
@@ -139,25 +137,23 @@ void GeckoAppShell::InitStubs(JNIEnv *jE
     jGetMessageWrapper = getStaticMethod("getMessage", "(II)V");
     jGetMimeTypeFromExtensionsWrapper = getStaticMethod("getMimeTypeFromExtensions", "(Ljava/lang/String;)Ljava/lang/String;");
     jGetNextMessageInListWrapper = getStaticMethod("getNextMessageInList", "(II)V");
     jGetProxyForURIWrapper = getStaticMethod("getProxyForURI", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
     jGetScreenDepthWrapper = getStaticMethod("getScreenDepth", "()I");
     jGetScreenOrientationWrapper = getStaticMethod("getScreenOrientation", "()S");
     jGetShowPasswordSetting = getStaticMethod("getShowPasswordSetting", "()Z");
     jGetSystemColoursWrapper = getStaticMethod("getSystemColors", "()[I");
-    jGetUserRestrictions = getStaticMethod("getUserRestrictions", "()Ljava/lang/String;");
     jHandleGeckoMessageWrapper = getStaticMethod("handleGeckoMessage", "(Lorg/mozilla/gecko/util/NativeJSContainer;)V");
     jHandleUncaughtException = getStaticMethod("handleUncaughtException", "(Ljava/lang/Thread;Ljava/lang/Throwable;)V");
     jHideProgressDialog = getStaticMethod("hideProgressDialog", "()V");
     jInitCameraWrapper = getStaticMethod("initCamera", "(Ljava/lang/String;III)[I");
     jIsNetworkLinkKnown = getStaticMethod("isNetworkLinkKnown", "()Z");
     jIsNetworkLinkUp = getStaticMethod("isNetworkLinkUp", "()Z");
     jIsTablet = getStaticMethod("isTablet", "()Z");
-    jIsUserRestricted = getStaticMethod("isUserRestricted", "()Z");
     jKillAnyZombies = getStaticMethod("killAnyZombies", "()V");
     jLoadPluginClass = getStaticMethod("loadPluginClass", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Class;");
     jLockScreenOrientation = getStaticMethod("lockScreenOrientation", "(I)V");
     jMarkURIVisited = getStaticMethod("markUriVisited", "(Ljava/lang/String;)V");
     jMoveTaskToBack = getStaticMethod("moveTaskToBack", "()V");
     jNetworkLinkType = getStaticMethod("networkLinkType", "()I");
     jNotifyDefaultPrevented = getStaticMethod("notifyDefaultPrevented", "(Z)V");
     jNotifyIME = getStaticMethod("notifyIME", "(I)V");
@@ -782,29 +778,16 @@ jintArray GeckoAppShell::GetSystemColour
     }
 
     jobject temp = env->CallStaticObjectMethod(mGeckoAppShellClass, jGetSystemColoursWrapper);
     AndroidBridge::HandleUncaughtException(env);
     jintArray ret = static_cast<jintArray>(env->PopLocalFrame(temp));
     return ret;
 }
 
-jstring GeckoAppShell::GetUserRestrictions() {
-    JNIEnv *env = AndroidBridge::GetJNIEnv();
-    if (env->PushLocalFrame(1) != 0) {
-        AndroidBridge::HandleUncaughtException(env);
-        MOZ_CRASH("Exception should have caused crash.");
-    }
-
-    jobject temp = env->CallStaticObjectMethod(mGeckoAppShellClass, jGetUserRestrictions);
-    AndroidBridge::HandleUncaughtException(env);
-    jstring ret = static_cast<jstring>(env->PopLocalFrame(temp));
-    return ret;
-}
-
 void GeckoAppShell::HandleGeckoMessageWrapper(jobject a0) {
     JNIEnv *env = AndroidBridge::GetJNIEnv();
     if (env->PushLocalFrame(1) != 0) {
         AndroidBridge::HandleUncaughtException(env);
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     env->CallStaticVoidMethod(mGeckoAppShellClass, jHandleGeckoMessageWrapper, a0);
@@ -887,29 +870,16 @@ bool GeckoAppShell::IsTablet() {
     }
 
     bool temp = env->CallStaticBooleanMethod(mGeckoAppShellClass, jIsTablet);
     AndroidBridge::HandleUncaughtException(env);
     env->PopLocalFrame(nullptr);
     return temp;
 }
 
-bool GeckoAppShell::IsUserRestricted() {
-    JNIEnv *env = AndroidBridge::GetJNIEnv();
-    if (env->PushLocalFrame(0) != 0) {
-        AndroidBridge::HandleUncaughtException(env);
-        MOZ_CRASH("Exception should have caused crash.");
-    }
-
-    bool temp = env->CallStaticBooleanMethod(mGeckoAppShellClass, jIsUserRestricted);
-    AndroidBridge::HandleUncaughtException(env);
-    env->PopLocalFrame(nullptr);
-    return temp;
-}
-
 void GeckoAppShell::KillAnyZombies() {
     JNIEnv *env = AndroidBridge::GetJNIEnv();
     if (env->PushLocalFrame(0) != 0) {
         AndroidBridge::HandleUncaughtException(env);
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     env->CallStaticVoidMethod(mGeckoAppShellClass, jKillAnyZombies);
@@ -1559,16 +1529,76 @@ void GeckoJavaSampler::UnpauseJavaProfil
         AndroidBridge::HandleUncaughtException(env);
         MOZ_CRASH("Exception should have caused crash.");
     }
 
     env->CallStaticVoidMethod(mGeckoJavaSamplerClass, jUnpauseJavaProfiling);
     AndroidBridge::HandleUncaughtException(env);
     env->PopLocalFrame(nullptr);
 }
+jclass RestrictedProfiles::mRestrictedProfilesClass = 0;
+jmethodID RestrictedProfiles::jGetUserRestrictions = 0;
+jmethodID RestrictedProfiles::jIsAllowed = 0;
+jmethodID RestrictedProfiles::jIsUserRestricted = 0;
+void RestrictedProfiles::InitStubs(JNIEnv *jEnv) {
+    initInit();
+
+    mRestrictedProfilesClass = getClassGlobalRef("org/mozilla/gecko/RestrictedProfiles");
+    jGetUserRestrictions = getStaticMethod("getUserRestrictions", "()Ljava/lang/String;");
+    jIsAllowed = getStaticMethod("isAllowed", "(ILjava/lang/String;)Z");
+    jIsUserRestricted = getStaticMethod("isUserRestricted", "()Z");
+}
+
+RestrictedProfiles* RestrictedProfiles::Wrap(jobject obj) {
+    JNIEnv *env = GetJNIForThread();
+    RestrictedProfiles* ret = new RestrictedProfiles(obj, env);
+    env->DeleteLocalRef(obj);
+    return ret;
+}
+
+jstring RestrictedProfiles::GetUserRestrictions() {
+    JNIEnv *env = AndroidBridge::GetJNIEnv();
+    if (env->PushLocalFrame(1) != 0) {
+        AndroidBridge::HandleUncaughtException(env);
+        MOZ_CRASH("Exception should have caused crash.");
+    }
+
+    jobject temp = env->CallStaticObjectMethod(mRestrictedProfilesClass, jGetUserRestrictions);
+    AndroidBridge::HandleUncaughtException(env);
+    jstring ret = static_cast<jstring>(env->PopLocalFrame(temp));
+    return ret;
+}
+
+bool RestrictedProfiles::IsAllowed(int32_t a0, const nsAString& a1) {
+    JNIEnv *env = AndroidBridge::GetJNIEnv();
+    if (env->PushLocalFrame(1) != 0) {
+        AndroidBridge::HandleUncaughtException(env);
+        MOZ_CRASH("Exception should have caused crash.");
+    }
+
+    jstring j1 = AndroidBridge::NewJavaString(env, a1);
+
+    bool temp = env->CallStaticBooleanMethod(mRestrictedProfilesClass, jIsAllowed, a0, j1);
+    AndroidBridge::HandleUncaughtException(env);
+    env->PopLocalFrame(nullptr);
+    return temp;
+}
+
+bool RestrictedProfiles::IsUserRestricted() {
+    JNIEnv *env = AndroidBridge::GetJNIEnv();
+    if (env->PushLocalFrame(0) != 0) {
+        AndroidBridge::HandleUncaughtException(env);
+        MOZ_CRASH("Exception should have caused crash.");
+    }
+
+    bool temp = env->CallStaticBooleanMethod(mRestrictedProfilesClass, jIsUserRestricted);
+    AndroidBridge::HandleUncaughtException(env);
+    env->PopLocalFrame(nullptr);
+    return temp;
+}
 jclass SurfaceBits::mSurfaceBitsClass = 0;
 jmethodID SurfaceBits::jSurfaceBits = 0;
 jfieldID SurfaceBits::jbuffer = 0;
 jfieldID SurfaceBits::jformat = 0;
 jfieldID SurfaceBits::jheight = 0;
 jfieldID SurfaceBits::jwidth = 0;
 void SurfaceBits::InitStubs(JNIEnv *jEnv) {
     initInit();
@@ -2540,16 +2570,17 @@ void Clipboard::SetClipboardText(const n
     AndroidBridge::HandleUncaughtException(env);
     env->PopLocalFrame(nullptr);
 }
 
 void InitStubs(JNIEnv *jEnv) {
     GeckoAppShell::InitStubs(jEnv);
     JavaDomKeyLocation::InitStubs(jEnv);
     GeckoJavaSampler::InitStubs(jEnv);
+    RestrictedProfiles::InitStubs(jEnv);
     SurfaceBits::InitStubs(jEnv);
     ThumbnailHelper::InitStubs(jEnv);
     DisplayPortMetrics::InitStubs(jEnv);
     GLController::InitStubs(jEnv);
     GeckoLayerClient::InitStubs(jEnv);
     ImmutableViewportMetrics::InitStubs(jEnv);
     LayerView::InitStubs(jEnv);
     NativePanZoomController::InitStubs(jEnv);
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -57,25 +57,23 @@ public:
     static void GetMessageWrapper(int32_t a0, int32_t a1);
     static jstring GetMimeTypeFromExtensionsWrapper(const nsAString& a0);
     static void GetNextMessageInListWrapper(int32_t a0, int32_t a1);
     static jstring GetProxyForURIWrapper(const nsAString& a0, const nsAString& a1, const nsAString& a2, int32_t a3);
     static int32_t GetScreenDepthWrapper();
     static int16_t GetScreenOrientationWrapper();
     static bool GetShowPasswordSetting();
     static jintArray GetSystemColoursWrapper();
-    static jstring GetUserRestrictions();
     static void HandleGeckoMessageWrapper(jobject a0);
     static void HandleUncaughtException(jobject a0, jthrowable a1);
     static void HideProgressDialog();
     static jintArray InitCameraWrapper(const nsAString& a0, int32_t a1, int32_t a2, int32_t a3);
     static bool IsNetworkLinkKnown();
     static bool IsNetworkLinkUp();
     static bool IsTablet();
-    static bool IsUserRestricted();
     static void KillAnyZombies();
     static jclass LoadPluginClass(const nsAString& a0, const nsAString& a1);
     static void LockScreenOrientation(int32_t a0);
     static void MarkURIVisited(const nsAString& a0);
     static void MoveTaskToBack();
     static int32_t NetworkLinkType();
     static void NotifyDefaultPrevented(bool a0);
     static void NotifyIME(int32_t a0);
@@ -145,25 +143,23 @@ protected:
     static jmethodID jGetMessageWrapper;
     static jmethodID jGetMimeTypeFromExtensionsWrapper;
     static jmethodID jGetNextMessageInListWrapper;
     static jmethodID jGetProxyForURIWrapper;
     static jmethodID jGetScreenDepthWrapper;
     static jmethodID jGetScreenOrientationWrapper;
     static jmethodID jGetShowPasswordSetting;
     static jmethodID jGetSystemColoursWrapper;
-    static jmethodID jGetUserRestrictions;
     static jmethodID jHandleGeckoMessageWrapper;
     static jmethodID jHandleUncaughtException;
     static jmethodID jHideProgressDialog;
     static jmethodID jInitCameraWrapper;
     static jmethodID jIsNetworkLinkKnown;
     static jmethodID jIsNetworkLinkUp;
     static jmethodID jIsTablet;
-    static jmethodID jIsUserRestricted;
     static jmethodID jKillAnyZombies;
     static jmethodID jLoadPluginClass;
     static jmethodID jLockScreenOrientation;
     static jmethodID jMarkURIVisited;
     static jmethodID jMoveTaskToBack;
     static jmethodID jNetworkLinkType;
     static jmethodID jNotifyDefaultPrevented;
     static jmethodID jNotifyIME;
@@ -241,16 +237,32 @@ protected:
     static jmethodID jGetSampleTimeJavaProfiling;
     static jmethodID jGetThreadNameJavaProfilingWrapper;
     static jmethodID jPauseJavaProfiling;
     static jmethodID jStartJavaProfiling;
     static jmethodID jStopJavaProfiling;
     static jmethodID jUnpauseJavaProfiling;
 };
 
+class RestrictedProfiles : public AutoGlobalWrappedJavaObject {
+public:
+    static void InitStubs(JNIEnv *jEnv);
+    static RestrictedProfiles* Wrap(jobject obj);
+    RestrictedProfiles(jobject obj, JNIEnv* env) : AutoGlobalWrappedJavaObject(obj, env) {};
+    static jstring GetUserRestrictions();
+    static bool IsAllowed(int32_t a0, const nsAString& a1);
+    static bool IsUserRestricted();
+    RestrictedProfiles() : AutoGlobalWrappedJavaObject() {};
+protected:
+    static jclass mRestrictedProfilesClass;
+    static jmethodID jGetUserRestrictions;
+    static jmethodID jIsAllowed;
+    static jmethodID jIsUserRestricted;
+};
+
 class SurfaceBits : public AutoGlobalWrappedJavaObject {
 public:
     static void InitStubs(JNIEnv *jEnv);
     static SurfaceBits* Wrap(jobject obj);
     SurfaceBits(jobject obj, JNIEnv* env) : AutoGlobalWrappedJavaObject(obj, env) {};
     SurfaceBits();
     jobject getbuffer();
     void setbuffer(jobject a0);