Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Mon, 02 Jun 2014 15:23:46 +0200
changeset 205383 f0c34fad69cb9a3e2117d31b3514a02a9470230d
parent 205382 743209d15d598a7f56f16fdd9be0edcc1971dcce (current diff)
parent 205309 56f9b7162d7c21891b27bb9fdb627474903a3dda (diff)
child 205384 2215d5520922741d3a9516adc16056ffb0bf7d3a
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
dom/ipc/ContentParent.cpp
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1058,16 +1058,17 @@ window.addEventListener('ContentStart', 
     },
     "ipc:content-shutdown", false);
 })();
 
 var CaptivePortalLoginHelper = {
   init: function init() {
     Services.obs.addObserver(this, 'captive-portal-login', false);
     Services.obs.addObserver(this, 'captive-portal-login-abort', false);
+    Services.obs.addObserver(this, 'captive-portal-login-success', false);
   },
   handleEvent: function handleEvent(detail) {
     Services.captivePortalDetector.cancelLogin(detail.id);
   },
   observe: function observe(subject, topic, data) {
     shell.sendChromeEvent(JSON.parse(data));
   }
 }
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,23 +14,23 @@
   <!--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="2a165bebfa19b11b697837409f9550dd2917c46c">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9100fa82fc355f5201e23e400fc6b40e875304ed"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <project name="librecovery" patch="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <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="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,23 +14,23 @@
   <!--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="2a165bebfa19b11b697837409f9550dd2917c46c">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9100fa82fc355f5201e23e400fc6b40e875304ed"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
@@ -113,17 +113,17 @@
   <project name="platform/system/media" path="system/media" revision="df2cdd433738a891102873710bdd3c3db7adb0cc"/>
   <project name="platform/system/netd" path="system/netd" revision="ea8103eae5642621ca8202e00620f4ca954ed413"/>
   <project name="platform/system/security" path="system/security" revision="360f51f7af191316cd739f229db1c5f7233be063"/>
   <project name="platform/system/vold" path="system/vold" revision="153df4d067a4149c7d78f1c92fed2ce2bd6a272e"/>
   <default remote="caf" revision="jb_3.2" sync-j="4"/>
   <!-- Flame specific things -->
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="e8a318f7690092e639ba88891606f4183e846d3f"/>
   <project name="device/qcom/common" path="device/qcom/common" revision="34ed8345250bb97262d70a052217a92e83444ede"/>
-  <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="218bcff6200f9b9e054467da963b7209e43ad287"/>
+  <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="76f960b9512ec5c4726b5f52dd94bdf3c07e5071"/>
   <project name="kernel/msm" path="kernel" revision="228d59147ff524e90774c566eef03260cc6857b8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="f2914eacee9120680a41463708bb6ee8291749fc"/>
   <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="81c4a859d75d413ad688067829d21b7ba9205f81"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="f0689ac1914cdbc59e53bdc9edd9013dc157c299"/>
   <project name="platform/external/bluetooth/glib" path="external/bluetooth/glib" revision="dd925f76e4f149c3d5571b80e12f7e24bbe89c59"/>
   <project name="platform/external/dbus" path="external/dbus" revision="ea87119c843116340f5df1d94eaf8275e1055ae8"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="494c177966fdc31183a5f7af82dc9130f523da4b"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="320b05a5761eb2a4816f7529c91ea49422979b55"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "46fb0be835267316bda52a12dedab53978456833", 
+    "revision": "01ae06e7d0c3c72d51e6801986339d6c06229c9b", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,22 +12,22 @@
   <!--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="2a165bebfa19b11b697837409f9550dd2917c46c">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <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="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
   <project name="platform/development" path="development" revision="2460485184bc8535440bb63876d4e63ec1b4770c"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="2a165bebfa19b11b697837409f9550dd2917c46c">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <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="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <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,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,22 +12,22 @@
   <!--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="2a165bebfa19b11b697837409f9550dd2917c46c">
     <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="e98fe1e94d3d80ad36903500d8ca3333904b162c"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="82679a5ce84d1b6bf388da6536d5682a3ad56de3"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
   <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="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="41e7db9834c5ed99ed448074dce2b7331cf19c9f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="c629a8c1e0101d3937ceb4c52a60f7569b9d4243"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
   <project name="device/sample" path="device/sample" revision="c328f3d4409db801628861baa8d279fb8855892f"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1462,17 +1462,17 @@ pref("browser.newtab.preload", true);
 pref("browser.newtabpage.enabled", true);
 
 // number of rows of newtab grid
 pref("browser.newtabpage.rows", 3);
 
 // number of columns of newtab grid
 pref("browser.newtabpage.columns", 3);
 
-pref("browser.newtabpage.directorySource", "chrome://global/content/directoryLinks.json");
+pref("browser.newtabpage.directory.source", "chrome://global/content/directoryLinks.json");
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // True if the fullscreen API requires approval upon a domain entering fullscreen.
 // Domains that have already had fullscreen permission granted won't re-request
 // approval.
 pref("full-screen-api.approval-required", true);
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -212,16 +212,17 @@ let gPage = {
         site.captureIfMissing();
         let {type} = site.link;
         if (type in directoryCount) {
           directoryCount[type]++;
         }
       }
     }
 
+    DirectoryLinksProvider.reportShownCount(directoryCount);
     // Record how many directory sites were shown, but place counts over the
     // default 9 in the same bucket
     for (let type of Object.keys(directoryCount)) {
       let count = directoryCount[type];
       let shownId = "NEWTAB_PAGE_DIRECTORY_" + type.toUpperCase() + "_SHOWN";
       let shownCount = Math.min(10, count);
       Services.telemetry.getHistogramById(shownId).add(shownCount);
     }
--- a/browser/base/content/test/general/browser_tabopen_reflows.js
+++ b/browser/base/content/test/general/browser_tabopen_reflows.js
@@ -51,44 +51,69 @@ const EXPECTED_REFLOWS = [
     "tabPreviews_handleEvent/<@chrome://browser/content/browser.js|",
 
   // tabPreviews.capture()
   "tabPreviews_capture@chrome://browser/content/browser.js|" +
     "@chrome://browser/content/browser.js|"
 ];
 
 const PREF_PRELOAD = "browser.newtab.preload";
-const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directorySource";
+const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source";
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new tabs.
  */
 function test() {
   waitForExplicitFinish();
+  let DirectoryLinksProvider = Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm", {}).DirectoryLinksProvider;
+  let NewTabUtils = Cu.import("resource://gre/modules/NewTabUtils.jsm", {}).NewTabUtils;
+  let Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
-  Services.prefs.setBoolPref(PREF_PRELOAD, false);
-  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json,{}");
+  // resolves promise when directory links are downloaded and written to disk
+  function watchLinksChangeOnce() {
+    let deferred = Promise.defer();
+    let observer = {
+      onManyLinksChanged: () => {
+        DirectoryLinksProvider.removeObserver(observer);
+        NewTabUtils.links.populateCache(() => {
+          NewTabUtils.allPages.update();
+          deferred.resolve();
+        }, true);
+      }
+    };
+    observer.onDownloadFail = observer.onManyLinksChanged;
+    DirectoryLinksProvider.addObserver(observer);
+    return deferred.promise;
+  };
+
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref(PREF_PRELOAD);
     Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
+    return watchLinksChangeOnce();
   });
 
-  // Add a reflow observer and open a new tab.
-  docShell.addWeakReflowObserver(observer);
-  BrowserOpenTab();
+  // run tests when directory source change completes
+  watchLinksChangeOnce().then(() => {
+    // Add a reflow observer and open a new tab.
+    docShell.addWeakReflowObserver(observer);
+    BrowserOpenTab();
 
-  // Wait until the tabopen animation has finished.
-  waitForTransitionEnd(function () {
-    // Remove reflow observer and clean up.
-    docShell.removeWeakReflowObserver(observer);
-    gBrowser.removeCurrentTab();
+    // Wait until the tabopen animation has finished.
+    waitForTransitionEnd(function () {
+      // Remove reflow observer and clean up.
+      docShell.removeWeakReflowObserver(observer);
+      gBrowser.removeCurrentTab();
+      finish();
+    });
+  });
 
-    finish();
-  });
+  Services.prefs.setBoolPref(PREF_PRELOAD, false);
+  // set directory source to empty links
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json,{}");
 }
 
 let observer = {
   reflow: function (start, end) {
     // Gather information about the current code path.
     let path = (new Error().stack).split("\n").slice(1).map(line => {
       return line.replace(/:\d+:\d+$/, "");
     }).join("|");
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -1,26 +1,25 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
-const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directorySource";
+const PREF_NEWTAB_DIRECTORYSOURCE = "browser.newtabpage.directory.source";
 
 Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
-// start with no directory links by default
-Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json,{}");
 
 let tmp = {};
 Cu.import("resource://gre/modules/Promise.jsm", tmp);
 Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp);
+Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm", tmp);
 Cc["@mozilla.org/moz/jssubscript-loader;1"]
   .getService(Ci.mozIJSSubScriptLoader)
   .loadSubScript("chrome://browser/content/sanitize.js", tmp);
 Cu.import("resource://gre/modules/Timer.jsm", tmp);
-let {Promise, NewTabUtils, Sanitizer, clearTimeout} = tmp;
+let {Promise, NewTabUtils, Sanitizer, clearTimeout, DirectoryLinksProvider} = tmp;
 
 let uri = Services.io.newURI("about:newtab", null, null);
 let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
 
 let isMac = ("nsILocalFileMac" in Ci);
 let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc);
 let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
 let gWindow = window;
@@ -55,44 +54,65 @@ if (gBrowser.contentWindow.innerHeight <
 
 registerCleanupFunction(function () {
   while (gWindow.gBrowser.tabs.length > 1)
     gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]);
 
   if (oldInnerHeight)
     gBrowser.contentWindow.innerHeight = oldInnerHeight;
 
-  Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
-  Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
-
   // Stop any update timers to prevent unexpected updates in later tests
   let timer = NewTabUtils.allPages._scheduleUpdateTimeout;
   if (timer) {
     clearTimeout(timer);
     delete NewTabUtils.allPages._scheduleUpdateTimeout;
   }
+
+  Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
+  Services.prefs.clearUserPref(PREF_NEWTAB_DIRECTORYSOURCE);
+
+  return watchLinksChangeOnce();
 });
 
 /**
+ * Resolves promise when directory links are downloaded and written to disk
+ */
+function watchLinksChangeOnce() {
+  let deferred = Promise.defer();
+  let observer = {
+    onManyLinksChanged: () => {
+      DirectoryLinksProvider.removeObserver(observer);
+      deferred.resolve();
+    }
+  };
+  observer.onDownloadFail = observer.onManyLinksChanged;
+  DirectoryLinksProvider.addObserver(observer);
+  return deferred.promise;
+};
+
+/**
  * Provide the default test function to start our test runner.
  */
 function test() {
-  TestRunner.run();
+  waitForExplicitFinish();
+  // start TestRunner.run() after directory links is downloaded and written to disk
+  watchLinksChangeOnce().then(() => {
+    TestRunner.run();
+  });
+  Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json,{}");
 }
 
 /**
  * The test runner that controls the execution flow of our tests.
  */
 let TestRunner = {
   /**
    * Starts the test runner.
    */
   run: function () {
-    waitForExplicitFinish();
-
     this._iter = runTests();
     this.next();
   },
 
   /**
    * Runs the next available test or finishes if there's no test left.
    */
   next: function () {
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -1171,20 +1171,33 @@ ContentParent::ShutDownProcess(bool aClo
     // NB: must MarkAsDead() here so that this isn't accidentally
     // returned from Get*() while in the midst of shutdown.
     MarkAsDead();
 
     // A ContentParent object might not get freed until after XPCOM shutdown has
     // shut down the cycle collector.  But by then it's too late to release any
     // CC'ed objects, so we need to null them out here, while we still can.  See
     // bug 899761.
-    if (mMessageManager) {
-      mMessageManager->Disconnect();
-      mMessageManager = nullptr;
-    }
+    ShutDownMessageManager();
+}
+
+void
+ContentParent::ShutDownMessageManager()
+{
+  if (!mMessageManager) {
+    return;
+  }
+
+  mMessageManager->ReceiveMessage(
+            static_cast<nsIContentFrameMessageManager*>(mMessageManager.get()),
+            CHILD_PROCESS_SHUTDOWN_MESSAGE, false,
+            nullptr, nullptr, nullptr, nullptr);
+
+  mMessageManager->Disconnect();
+  mMessageManager = nullptr;
 }
 
 void
 ContentParent::MarkAsDead()
 {
     if (!mAppManifestURL.IsEmpty()) {
         if (sAppContentParents) {
             sAppContentParents->Remove(mAppManifestURL);
@@ -1300,36 +1313,28 @@ struct DelayedDeleteContentParentTask : 
 void
 ContentParent::ActorDestroy(ActorDestroyReason why)
 {
     if (mForceKillTask) {
         mForceKillTask->Cancel();
         mForceKillTask = nullptr;
     }
 
-    nsRefPtr<nsFrameMessageManager> ppm = mMessageManager;
-    if (ppm) {
-      ppm->ReceiveMessage(static_cast<nsIContentFrameMessageManager*>(ppm.get()),
-                          CHILD_PROCESS_SHUTDOWN_MESSAGE, false,
-                          nullptr, nullptr, nullptr, nullptr);
-    }
+    ShutDownMessageManager();
+
     nsRefPtr<ContentParent> kungFuDeathGrip(this);
     nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
     if (obs) {
         size_t length = ArrayLength(sObserverTopics);
         for (size_t i = 0; i < length; ++i) {
             obs->RemoveObserver(static_cast<nsIObserver*>(this),
                                 sObserverTopics[i]);
         }
     }
 
-    if (ppm) {
-      ppm->Disconnect();
-    }
-
     // Tell the memory reporter manager that this ContentParent is going away.
     nsRefPtr<nsMemoryReporterManager> mgr =
         nsMemoryReporterManager::GetOrCreate();
 #ifdef MOZ_NUWA_PROCESS
     bool isMemoryChild = !IsNuwaProcess();
 #else
     bool isMemoryChild = true;
 #endif
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -329,16 +329,20 @@ private:
      *
      * If aCloseWithError is true and this is the first call to
      * ShutDownProcess, then we'll close our channel using CloseWithError()
      * rather than vanilla Close().  CloseWithError() indicates to IPC that this
      * is an abnormal shutdown (e.g. a crash).
      */
     void ShutDownProcess(bool aCloseWithError);
 
+    // Perform any steps necesssary to gracefully shtudown the message
+    // manager and null out mMessageManager.
+    void ShutDownMessageManager();
+
     PCompositorParent*
     AllocPCompositorParent(mozilla::ipc::Transport* aTransport,
                            base::ProcessId aOtherProcess) MOZ_OVERRIDE;
     PImageBridgeParent*
     AllocPImageBridgeParent(mozilla::ipc::Transport* aTransport,
                             base::ProcessId aOtherProcess) MOZ_OVERRIDE;
 
     PSharedBufferManagerParent*
--- a/dom/messages/SystemMessagePermissionsChecker.jsm
+++ b/dom/messages/SystemMessagePermissionsChecker.jsm
@@ -51,16 +51,19 @@ this.SystemMessagePermissionsTable = {
   },
   "bluetooth-opp-receiving-file-confirmation": {
     "bluetooth": []
   },
   "bluetooth-opp-transfer-start": {
     "bluetooth": []
   },
   "connection": { },
+  "captive-portal": {
+    "wifi-manage": []
+  },
   "dummy-system-message": { }, // for system message testing framework
   "headset-button": { },
   "icc-stkcommand": {
     "settings": ["read", "write"]
   },
   "media-button": { },
   "networkstats-alarm": {
     "networkstats-manage": []
--- a/dom/settings/tests/test_settings_blobs.html
+++ b/dom/settings/tests/test_settings_blobs.html
@@ -40,17 +40,17 @@ function onFailure() {
       ok(false, "in on Failure!");
     }
   }
 }
 
 let mozSettings = window.navigator.mozSettings;
 let req;
 
-let storedBlob = new Blob([""], {"type": "text/xml"});
+let storedBlob = new Blob(['12345'], {"type": "text/plain"});
 
 function checkBlob(blob) {
   try {
     let url = URL.createObjectURL(blob);
     ok(true, "Valid blob");
   } catch (e) {
     ok(false, "Valid blob");
   }
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/head.js
@@ -0,0 +1,731 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let Promise = SpecialPowers.Cu.import('resource://gre/modules/Promise.jsm').Promise;
+
+const STOCK_HOSTAPD_NAME = 'goldfish-hostapd';
+const HOSTAPD_CONFIG_PATH = '/data/misc/wifi/hostapd/';
+
+const HOSTAPD_COMMON_CONFIG = {
+  driver: 'test',
+  ctrl_interface: '/data/misc/wifi/hostapd',
+  test_socket: 'DIR:/data/misc/wifi/sockets',
+  hw_mode: 'b',
+  channel: '2',
+};
+
+const HOSTAPD_CONFIG_LIST = [
+  { ssid: 'ap0' },
+
+  { ssid: 'ap1',
+    wpa: 1,
+    wpa_pairwise: 'TKIP CCMP',
+    wpa_passphrase: '12345678'
+  },
+
+  { ssid: 'ap2',
+    wpa: 2,
+    rsn_pairwise: 'CCMP',
+    wpa_passphrase: '12345678',
+  },
+];
+
+let gTestSuite = (function() {
+  let suite = {};
+
+  // Private member variables of the returned object |suite|.
+  let wifiManager;
+  let wifiOrigEnabled;
+  let pendingEmulatorShellCount = 0;
+
+  /**
+   * Send emulator shell command with safe guard.
+   *
+   * We should only call |finish()| after all emulator command transactions
+   * end, so here comes with the pending counter.  Resolve when the emulator
+   * gives positive response, and reject otherwise.
+   *
+   * Fulfill params:
+   *   result -- an array of emulator response lines.
+   * Reject params:
+   *   result -- an array of emulator response lines.
+   *
+   * @param aCommand
+   *        A string command to be passed to emulator through its telnet console.
+   *
+   * @return A deferred promise.
+   */
+  function runEmulatorShellSafe(aCommand) {
+    let deferred = Promise.defer();
+
+    ++pendingEmulatorShellCount;
+    runEmulatorShell(aCommand, function(aResult) {
+      --pendingEmulatorShellCount;
+
+      ok(true, "Emulator shell response: " + JSON.stringify(aResult));
+      if (Array.isArray(aResult)) {
+        deferred.resolve(aResult);
+      } else {
+        deferred.reject(aResult);
+      }
+    });
+
+    return deferred.promise;
+  }
+
+  /**
+   * Wait for one named MozWifiManager event.
+   *
+   * Resolve if that named event occurs.  Never reject.
+   *
+   * Fulfill params: the DOMEvent passed.
+   *
+   * @param aEventName
+   *        A string event name.
+   *
+   * @return A deferred promise.
+   */
+  function waitForWifiManagerEventOnce(aEventName) {
+    let deferred = Promise.defer();
+
+    wifiManager.addEventListener(aEventName, function onevent(aEvent) {
+      wifiManager.removeEventListener(aEventName, onevent);
+
+      ok(true, "WifiManager event '" + aEventName + "' got.");
+      deferred.resolve(aEvent);
+    });
+
+    return deferred.promise;
+  }
+
+  /**
+   * Get the detail of currently running processes containing the given name.
+   *
+   * Use shell command 'ps' to get the desired process's detail. Never reject.
+   *
+   * Fulfill params:
+   *   result -- an array of { pname, pid }
+   *
+   * @param aProcessName
+   *        The process to get the detail.
+   *
+   * @return A deferred promise.
+   */
+  function getProcessDetail(aProcessName) {
+    return runEmulatorShellSafe(['ps'])
+      .then(processes => {
+        // Sample 'ps' output:
+        //
+        // USER     PID   PPID  VSIZE  RSS     WCHAN    PC         NAME
+        // root      1     0     284    204   c009e6c4 0000deb4 S /init
+        // root      2     0     0      0     c0052c64 00000000 S kthreadd
+        // root      3     2     0      0     c0044978 00000000 S ksoftirqd/0
+        //
+        let detail = [];
+
+        processes.shift(); // Skip the first line.
+        for (let i = 0; i < processes.length; i++) {
+          let tokens = processes[i].split(/\s+/);
+          let pname = tokens[tokens.length - 1];
+          let pid = tokens[1];
+          if (-1 !== pname.indexOf(aProcessName)) {
+            detail.push({ pname: pname, pid: pid });
+          }
+        }
+
+        return detail;
+      });
+  }
+
+  /**
+   * Add required permissions for wifi testing. Never reject.
+   *
+   * The permissions required for wifi testing are 'wifi-manage' and 'settings-write'.
+   * Never reject.
+   *
+   * Fulfill params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function addRequiredPermissions() {
+    let deferred = Promise.defer();
+
+    let permissions = [{ 'type': 'wifi-manage', 'allow': 1, 'context': window.document },
+                       { 'type': 'settings-write', 'allow': 1, 'context': window.document }];
+
+    SpecialPowers.pushPermissions(permissions, function() {
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  }
+
+  /**
+   * Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject.
+   *
+   * Fulfill params: A DOMEvent.
+   * Reject params: A DOMEvent.
+   *
+   * @param aRequest
+   *        A DOMRequest instance.
+   *
+   * @return A deferred promise.
+   */
+  function wrapDomRequestAsPromise(aRequest) {
+    let deferred = Promise.defer();
+
+    ok(aRequest instanceof DOMRequest,
+       "aRequest is instanceof " + aRequest.constructor);
+
+    aRequest.addEventListener("success", function(aEvent) {
+      deferred.resolve(aEvent);
+    });
+    aRequest.addEventListener("error", function(aEvent) {
+      deferred.reject(aEvent);
+    });
+
+    return deferred.promise;
+  }
+
+  /**
+   * Ensure wifi is enabled/disabled.
+   *
+   * Issue a wifi enable/disable request if wifi is not in the desired state;
+   * return a resolved promise otherwise. Note that you cannot rely on this
+   * function to test the correctness of enabling/disabling wifi.
+   * (use requestWifiEnabled instead)
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @return a resolved promise or deferred promise.
+   */
+  function ensureWifiEnabled(aEnabled) {
+    if (wifiManager.enabled === aEnabled) {
+      log('Already ' + (aEnabled ? 'enabled' : 'disabled'));
+      return Promise.resolve();
+    }
+    return requestWifiEnabled(aEnabled);
+  }
+
+  /**
+   * Issue a request to enable/disable wifi.
+   *
+   * For current design, this function will attempt to enable/disable wifi by
+   * writing 'wifi.enabled' regardless of the wifi state.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function requestWifiEnabled(aEnabled) {
+    return Promise.all([
+      waitForWifiManagerEventOnce(aEnabled ? 'enabled' : 'disabled'),
+      setSettings({ 'wifi.enabled': aEnabled }),
+    ]);
+  }
+
+  /**
+   * Issue a request to scan all wifi available networks.
+   *
+   * Resolve when we get the scan result; reject when any error
+   * occurs.
+   *
+   * Fulfill params: An array of MozWifiNetwork
+   * Reject params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function requestWifiScan() {
+    let request = wifiManager.getNetworks();
+    return wrapDomRequestAsPromise(request)
+      .then(event => event.target.result);
+  }
+
+  /**
+   * Request wifi scan and verify the scan result as well.
+   *
+   * Issue a wifi scan request and check if the result is expected.
+   * Since the old APs may be cached and the newly added APs may be
+   * still not scan-able, a couple of attempts are acceptable.
+   * Resolve if we eventually get the expected scan result; reject otherwise.
+   *
+   * Fulfill params: The scan result, which is an array of MozWifiNetwork
+   * Reject params: (none)
+   *
+   * @param aRetryCnt
+   *        The maxmimum number of attempts until we get the expected scan result.
+   * @param aExpectedNetworks
+   *        An array of object, each of which contains at least the |ssid| property.
+   *
+   * @return A deferred promise.
+   */
+  function testWifiScanWithRetry(aRetryCnt, aExpectedNetworks) {
+
+    // Check if every single ssid of each |aScanResult| exists in |aExpectedNetworks|
+    // as well as the length of |aScanResult| equals to |aExpectedNetworks|.
+    function isScanResultExpected(aScanResult) {
+      if (aScanResult.length !== aExpectedNetworks.length) {
+        return false;
+      }
+
+      for (let i = 0; i < aScanResult.length; i++) {
+        if (-1 === getFirstIndexBySsid(aScanResult[i].ssid, aExpectedNetworks)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    return requestWifiScan()
+      .then(function (networks) {
+        if (isScanResultExpected(networks, aExpectedNetworks)) {
+          return networks;
+        }
+        if (aRetryCnt > 0) {
+          return testWifiScanWithRetry(aRetryCnt - 1, aExpectedNetworks);
+        }
+        throw 'Unexpected scan result!';
+      });
+  }
+
+  /**
+   * Set mozSettings values.
+   *
+   * Resolve if that mozSettings value is set successfully, reject otherwise.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aSettings
+   *        An object of format |{key1: value1, key2: value2, ...}|.
+   * @param aAllowError [optional]
+   *        A boolean value.  If set to true, an error response won't be treated
+   *        as test failure.  Default: false.
+   *
+   * @return A deferred promise.
+   */
+  function setSettings(aSettings, aAllowError) {
+    let request = window.navigator.mozSettings.createLock().set(aSettings);
+    return wrapDomRequestAsPromise(request)
+      .then(function resolve() {
+        ok(true, "setSettings(" + JSON.stringify(aSettings) + ")");
+      }, function reject() {
+        ok(aAllowError, "setSettings(" + JSON.stringify(aSettings) + ")");
+      });
+  }
+
+  /**
+   * Start hostapd processes with given configuration list.
+   *
+   * For starting one hostapd, we need to generate a specific config file
+   * then launch a hostapd process with the confg file path passed. The
+   * config file is generated by two sources: one is the common
+   * part (HOSTAPD_COMMON_CONFIG) and the other is from the given |aConfigList|.
+   * Resolve when all the hostpads are requested to start. It is not guaranteed
+   * that all the hostapds will be up and running successfully. Never reject.
+   *
+   * Fulfill params: (none)
+   *
+   * @param aConfigList
+   *        An array of config objects, each property in which will be
+   *        output to the confg file with the format: [key]=[value] in one line.
+   *        See http://hostap.epitest.fi/cgit/hostap/plain/hostapd/hostapd.conf
+   *        for more information.
+   *
+   * @return A deferred promise.
+   */
+  function startHostapds(aConfigList) {
+
+    function createConfigFromCommon(aIndex) {
+      // Create an copy of HOSTAPD_COMMON_CONFIG.
+      let config = JSON.parse(JSON.stringify(HOSTAPD_COMMON_CONFIG));
+
+      // Add user config.
+      for (let key in aConfigList[aIndex]) {
+        config[key] = aConfigList[aIndex][key];
+      }
+
+      // 'interface' is a required field but no need of being configurable
+      // for a test case. So we initialize this field on our own.
+      config.interface = 'AP-' + aIndex;
+
+      return config;
+    }
+
+    function startOneHostapd(aIndex) {
+      let configFileName = HOSTAPD_CONFIG_PATH + 'ap' + aIndex + '.conf';
+      return writeHostapdConfFile(configFileName, createConfigFromCommon(aIndex))
+        .then(() => runEmulatorShellSafe(['hostapd', '-B', configFileName]))
+        .then(function (reply) {
+          // It may fail at the first time due to the previous ungracefully terminated one.
+          if (reply[0] === 'bind(PF_UNIX): Address already in use') {
+            return startOneHostapd(aIndex);
+          }
+        });
+    }
+
+    return Promise.all(aConfigList.map(function(aConfig, aIndex) {
+      return startOneHostapd(aIndex);
+    }));
+  }
+
+  /**
+   * Kill all the running hostapd processes.
+   *
+   * Use shell command 'kill -9' to kill all hostapds. Never reject.
+   *
+   * Fulfill params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function killAllHostapd() {
+    return getProcessDetail('hostapd')
+      .then(function (runningHostapds) {
+        let promises = runningHostapds.map(runningHostapd => {
+          return runEmulatorShellSafe(['kill', '-9', runningHostapd.pid]);
+        });
+        return Promise.all(promises);
+      });
+  }
+
+  /**
+   * Write out the config file to the given path.
+   *
+   * For each key/value pair in |aConfig|,
+   *
+   * [key]=[value]
+   *
+   * will be output to one new line. Never reject.
+   *
+   * Fulfill params: (none)
+   *
+   * @param aFilePath
+   *        The file path that we desire the config file to be located.
+   *
+   * @param aConfig
+   *        The config object.
+   *
+   * @return A deferred promise.
+   */
+  function writeHostapdConfFile(aFilePath, aConfig) {
+    let content = '';
+    for (let key in aConfig) {
+      if (aConfig.hasOwnProperty(key)) {
+        content += (key + '=' + aConfig[key] + '\n');
+      }
+    }
+    return writeFile(aFilePath, content);
+  }
+
+  /**
+   * Write file to the given path filled with given content.
+   *
+   * For now it is implemented by shell command 'echo'. Also, if the
+   * content contains whitespace, we need to quote the content to
+   * avoid error. Never reject.
+   *
+   * Fulfill params: (none)
+   *
+   * @param aFilePath
+   *        The file path that we desire the file to be located.
+   *
+   * @param aContent
+   *        The content as string which should be written to the file.
+   *
+   * @return A deferred promise.
+   */
+  function writeFile(aFilePath, aContent) {
+    if (-1 === aContent.indexOf(' ')) {
+      aContent = '"' + aContent + '"';
+    }
+    return runEmulatorShellSafe(['echo', aContent, '>', aFilePath]);
+  }
+
+  /**
+   * Check if a init service is running or not.
+   *
+   * Check the android property 'init.svc.[aServiceName]' to determine if
+   * a init service is running. Reject if the propery is neither 'running'
+   * nor 'stopped'.
+   *
+   * Fulfill params:
+   *   result -- |true| if the init service is running; |false| otherwise.
+   * Reject params: (none)
+   *
+   * @param aServiceName
+   *        The init service name.
+   *
+   * @return A deferred promise.
+   */
+  function isInitServiceRunning(aServiceName) {
+    return runEmulatorShellSafe(['getprop', 'init.svc.' + aServiceName])
+      .then(function (result) {
+        if ('running' !== result[0] && 'stopped' !== result[0]) {
+          throw 'Init service running state should be "running" or "stopped".';
+        }
+        return 'running' === result[0];
+      });
+  }
+
+  /**
+   * Wait for timeout.
+   *
+   * Resolve when the given duration elapsed. Never reject.
+   *
+   * Fulfill params: (none)
+   *
+   * @param aTimeoutMs
+   *        The duration after which the timeout event should occurs.
+   *
+   * @return A deferred promise.
+   */
+  function waitForTimeout(aTimeoutMs) {
+    let deferred = Promise.defer();
+
+    setTimeout(function() {
+      deferred.resolve();
+    }, aTimeoutMs);
+
+    return deferred.promise;
+  }
+
+  /**
+   * Start or stop an init service.
+   *
+   * Use shell command 'start'/'stop' to start/stop an init service.
+   * The running state will also be checked after we start/stop the service.
+   * Resolve if the service is successfully started/stopped; Reject otherwise.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aServiceName
+   *        The name of the service we want to start/stop.
+   *
+   * @param aStart
+   *        |true| for starting the init service. |false| for stopping.
+   *
+   * @return A deferred promise.
+   */
+  function startStopInitService(aServiceName, aStart) {
+    let retryCnt = 5;
+
+    return runEmulatorShellSafe([aStart ? 'start' : 'stop', aServiceName])
+      .then(() => isInitServiceRunning(aServiceName))
+      .then(function onIsServiceRunningResolved(aIsRunning) {
+        if (aStart === aIsRunning) {
+          return;
+        }
+
+        if (retryCnt-- > 0) {
+          log('Failed to ' + (aStart ? 'start ' : 'stop ') + aServiceName +
+              '. Retry: ' + retryCnt);
+
+          return waitForTimeout(500)
+            .then(() => isInitServiceRunning(aServiceName))
+            .then(onIsServiceRunningResolved);
+        }
+
+        throw 'Failed to ' + (aStart ? 'start' : 'stop') + ' ' + aServiceName;
+      });
+  }
+
+  /**
+   * Start the stock hostapd.
+   *
+   * Since the stock hostapd is an init service, use |startStopInitService| to
+   * start it. Note that we might fail to start the stock hostapd at the first time
+   * for unknown reason so give it the second chance to start again.
+   * Resolve when we are eventually successful to start the stock hostapd; Reject
+   * otherwise.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function startStockHostapd() {
+    return startStopInitService(STOCK_HOSTAPD_NAME, true)
+      .then(null, function onreject() {
+        log('Failed to restart goldfish-hostapd at the first time. Try again!');
+        return startStopInitService((STOCK_HOSTAPD_NAME), true);
+      });
+  }
+
+  /**
+   * Stop the stock hostapd.
+   *
+   * Since the stock hostapd is an init service, use |startStopInitService| to
+   * stop it.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function stopStockHostapd() {
+    return startStopInitService(STOCK_HOSTAPD_NAME, false);
+  }
+
+  /**
+   * Get the index of the first matching entry by |ssid|.
+   *
+   * Find the index of the first entry of |aArray| which property |ssid|
+   * is same as |aSsid|.
+   *
+   * @param aSsid
+   *        The ssid that we want to match.
+   * @param aArray
+   *        An array of objects, each of which should have the property |ssid|.
+   *
+   * @return The 0-based index of first matching entry if found; -1 otherwise.
+   */
+  function getFirstIndexBySsid(aSsid, aArray) {
+    for (let i = 0; i < aArray.length; i++) {
+      if (aArray[i].ssid === aSsid) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Count the number of running process and verify if the count is expected.
+   *
+   * Return a promise that resolves when the process has expected number
+   * of running instances and rejects otherwise.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aOrigWifiEnabled
+   *        Boolean which indicates wifi was originally enabled.
+   *
+   * @return A deferred promise.
+   */
+  function verifyNumOfProcesses(aProcessName, aExpectedNum) {
+    return getProcessDetail(aProcessName)
+      .then(function (detail) {
+        if (detail.length === aExpectedNum) {
+          return;
+        }
+        throw 'Unexpected number of running processes:' + aProcessName +
+              ', expected: ' + aExpectedNum + ', actual: ' + detail.length;
+      });
+  }
+
+  /**
+   * Clean up all the allocated resources and running services for the test.
+   *
+   * After the test no matter success or failure, we should
+   * 1) Restore to the wifi original state (enabled or disabled)
+   * 2) Wait until all pending emulator shell commands are done.
+   *
+   * |finsih| will be called in the end.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @return A deferred promise.
+   */
+  function cleanUp() {
+    waitFor(function() {
+      return ensureWifiEnabled(wifiOrigEnabled)
+        .then(finish);
+    }, function() {
+      return pendingEmulatorShellCount === 0;
+    });
+  }
+
+  /**
+   * Init the test environment.
+   *
+   * Mainly add the required permissions and initialize the wifiManager
+   * and the orignal state of wifi. Reject if failing to create
+   * window.navigator.mozWifiManager; resolve if all is well.
+   *
+   * |finsih| will be called in the end.
+   *
+   * Fulfill params: (none)
+   * Reject params: The reject reason.
+   *
+   * @return A deferred promise.
+   */
+  function initTestEnvironment() {
+    return addRequiredPermissions()
+      .then(function() {
+        wifiManager = window.navigator.mozWifiManager;
+        if (!wifiManager) {
+          throw 'window.navigator.mozWifiManager is NULL';
+        }
+        wifiOrigEnabled = wifiManager.enabled;
+      });
+  }
+
+  //---------------------------------------------------
+  // Public test suite functions
+  //---------------------------------------------------
+  suite.getWifiManager = (() => wifiManager);
+  suite.ensureWifiEnabled = ensureWifiEnabled;
+  suite.requestWifiEnabled = requestWifiEnabled;
+  suite.startHostapds = startHostapds;
+  suite.getProcessDetail = getProcessDetail;
+  suite.killAllHostapd = killAllHostapd;
+  suite.wrapDomRequestAsPromise = wrapDomRequestAsPromise;
+  suite.waitForWifiManagerEventOnce = waitForWifiManagerEventOnce;
+  suite.verifyNumOfProcesses = verifyNumOfProcesses;
+  suite.testWifiScanWithRetry = testWifiScanWithRetry;
+  suite.getFirstIndexBySsid = getFirstIndexBySsid;
+
+  /**
+   * Common test routine.
+   *
+   * Start a test with the given test case chain. The test environment will be
+   * settled down before the test. After the test, all the affected things will
+   * be restored.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aTestCaseChain
+   *        The test case entry point, which can be a function or a promise.
+   *
+   * @return A deferred promise.
+   */
+  suite.doTest = function(aTestCaseChain) {
+    return initTestEnvironment()
+      .then(aTestCaseChain)
+      .then(function onresolve() {
+        cleanUp();
+      }, function onreject(aReason) {
+        ok(false, 'Promise rejects during test' + (aReason ? '(' + aReason + ')' : ''));
+        cleanUp();
+      });
+  };
+
+  /**
+   * Common test routine without the presence of stock hostapd.
+   *
+   * Same as doTest except stopping the stock hostapd before test
+   * and restarting it after test.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aTestCaseChain
+   *        The test case entry point, which can be a function or a promise.
+   *
+   * @return A deferred promise.
+   */
+  suite.doTestWithoutStockAp = function(aTestCaseChain) {
+    return suite.doTest(function() {
+      return stopStockHostapd()
+        .then(aTestCaseChain)
+        .then(startStockHostapd);
+    });
+  };
+
+  return suite;
+})();
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/manifest.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+b2g = true
+browser = false
+qemu = true
+
+[test_wifi_enable.js]
+[test_wifi_scan.js]
+[test_wifi_associate.js]
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/test_wifi_associate.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+const SCAN_RETRY_CNT = 5;
+
+/**
+ * Test wifi association.
+ *
+ * Associate with the given network object which is obtained by
+ * MozWifiManager.getNetworks() (i.e. MozWifiNetwork).
+ * Resolve when the 'connected' status change event is received.
+ * Note that we might see other events like 'connecting'
+ * before 'connected'. So we need to call |waitForWifiManagerEventOnce|
+ * again whenever non 'connected' event is seen. Never reject.
+ *
+ * Fulfill params: (none)
+ *
+ * @param aNetwork
+ *        An object of MozWifiNetwork.
+ *
+ * @return A deferred promise.
+ */
+function testAssociate(aNetwork) {
+  if (!setPasswordIfNeeded(aNetwork)) {
+    throw 'Failed to set password';
+  }
+
+  function waitForConnected() {
+    return gTestSuite.waitForWifiManagerEventOnce('statuschange')
+      .then(function onstatuschange(event) {
+        log("event.status: " + event.status);
+        log("event.network.ssid: " + (event.network ? event.network.ssid : ''));
+
+        if ("connected" === event.status &&
+            event.network.ssid === aNetwork.ssid) {
+          return; // Got expected 'connected' event from aNetwork.ssid.
+        }
+
+        log('Not expected "connected" statuschange event. Wait again!');
+        return waitForConnected();
+      });
+  }
+
+  let promises = [];
+
+  // Register the event listerner to wait for 'connected' event first
+  // to avoid racing issue.
+  promises.push(waitForConnected());
+
+  // Then we do the association.
+  let request = gTestSuite.getWifiManager().associate(aNetwork);
+  promises.push(gTestSuite.wrapDomRequestAsPromise(request));
+
+  return Promise.all(promises);
+}
+
+/**
+ * Convert the given MozWifiNetwork object array to testAssociate chain.
+ *
+ * @param aNetworks
+ *        An array of MozWifiNetwork which we want to convert.
+ *
+ * @return A promise chain which "then"s testAssociate accordingly.
+ */
+function convertToTestAssociateChain(aNetworks) {
+  let chain = Promise.resolve();
+
+  aNetworks.forEach(function (aNetwork) {
+    chain = chain.then(() => testAssociate(aNetwork));
+  });
+
+  return chain;
+}
+
+/**
+ * Set the password for associating the given network if needed.
+ *
+ * Set the password by looking up HOSTAPD_CONFIG_LIST. This function
+ * will also set |keyManagement| properly.
+ *
+ * @param aNetwork
+ *        The MozWifiNetwork object.
+ *
+ * @return |true| if either insesure or successfully set the password/keyManagement.
+ *         |false| if the given network is not found in HOSTAPD_CONFIG_LIST.
+ */
+function setPasswordIfNeeded(aNetwork) {
+  let i = gTestSuite.getFirstIndexBySsid(aNetwork.ssid, HOSTAPD_CONFIG_LIST);
+  if (-1 === i) {
+    log('unknown ssid: ' + aNetwork.ssid);
+    return false; // Error!
+  }
+
+  if (!aNetwork.security.length) {
+    return true; // No need to set password.
+  }
+
+  let security = aNetwork.security[0];
+  if (/PSK$/.test(security)) {
+    aNetwork.psk = HOSTAPD_CONFIG_LIST[i].wpa_passphrase;
+    aNetwork.keyManagement = 'WPA-PSK';
+  } else if (/WEP$/.test(security)) {
+    aNetwork.wep = HOSTAPD_CONFIG_LIST[i].wpa_passphrase;
+    aNetwork.keyManagement = 'WEP';
+  }
+
+  return true;
+}
+
+gTestSuite.doTestWithoutStockAp(function() {
+  return gTestSuite.ensureWifiEnabled(true)
+    .then(() => gTestSuite.startHostapds(HOSTAPD_CONFIG_LIST))
+    .then(() => gTestSuite.verifyNumOfProcesses('hostapd', HOSTAPD_CONFIG_LIST.length))
+    .then(() => gTestSuite.testWifiScanWithRetry(SCAN_RETRY_CNT, HOSTAPD_CONFIG_LIST))
+    .then(networks => convertToTestAssociateChain(networks))
+    .then(gTestSuite.killAllHostapd)
+    .then(() => gTestSuite.verifyNumOfProcesses('hostapd', 0));
+});
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/test_wifi_enable.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+gTestSuite.doTest(function() {
+  return Promise.resolve()
+    .then(() => gTestSuite.ensureWifiEnabled(false))
+    .then(() => gTestSuite.requestWifiEnabled(true));
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/test_wifi_scan.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+const SCAN_RETRY_CNT = 5;
+
+/**
+ * Test scan with no AP present.
+ *
+ * The precondition is:
+ *   1) Wifi is enabled.
+ *   2) All the hostapds are turned off.
+ *
+ * @return deferred promise.
+ */
+function testScanNoAp() {
+  return gTestSuite.testWifiScanWithRetry(SCAN_RETRY_CNT, []);
+}
+
+/**
+ * Test scan with APs present.
+ *
+ * The precondition is:
+ *   1) Wifi is enabled.
+ *   2) All the hostapds are turned off.
+ *
+ * @return deferred promise.
+ */
+function testScanWithAps() {
+  return gTestSuite.startHostapds(HOSTAPD_CONFIG_LIST)
+    .then(() => gTestSuite.verifyNumOfProcesses('hostapd', HOSTAPD_CONFIG_LIST.length))
+    .then(() => gTestSuite.testWifiScanWithRetry(SCAN_RETRY_CNT, HOSTAPD_CONFIG_LIST))
+    .then(gTestSuite.killAllHostapd)
+    .then(() => gTestSuite.verifyNumOfProcesses('hostapd', 0));
+}
+
+gTestSuite.doTestWithoutStockAp(function() {
+  return gTestSuite.ensureWifiEnabled(true)
+    .then(testScanNoAp)
+    .then(testScanWithAps);
+});
--- a/testing/marionette/client/marionette/tests/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit-tests.ini
@@ -21,8 +21,9 @@ skip = false
 [include:../../../../../dom/battery/test/marionette/manifest.ini]
 [include:../../../../../dom/mobilemessage/tests/marionette/manifest.ini]
 [include:../../../../../dom/mobileconnection/tests/marionette/manifest.ini]
 [include:../../../../../dom/system/gonk/tests/marionette/manifest.ini]
 [include:../../../../../dom/icc/tests/marionette/manifest.ini]
 [include:../../../../../dom/system/tests/marionette/manifest.ini]
 [include:../../../../../dom/nfc/tests/marionette/manifest.ini]
 [include:../../../../../dom/events/test/marionette/manifest.ini]
+[include:../../../../../dom/wifi/test/marionette/manifest.ini]
--- a/testing/xpcshell/xpcshell_b2g.ini
+++ b/testing/xpcshell/xpcshell_b2g.ini
@@ -3,16 +3,17 @@
 ; file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 [include:dom/apps/tests/unit/xpcshell.ini]
 [include:dom/mobilemessage/tests/xpcshell.ini]
 [include:dom/network/tests/unit_stats/xpcshell.ini]
 [include:dom/system/gonk/tests/xpcshell.ini]
 [include:dom/wappush/tests/xpcshell.ini]
 [include:toolkit/components/osfile/tests/xpcshell/xpcshell.ini]
+[include:toolkit/components/captivedetect/test/unit/xpcshell.ini]
 [include:toolkit/devtools/apps/tests/unit/xpcshell.ini]
 [include:toolkit/devtools/debugger/tests/unit/xpcshell.ini]
 [include:toolkit/devtools/qrcode/tests/unit/xpcshell.ini]
 [include:toolkit/devtools/sourcemap/tests/unit/xpcshell.ini]
 [include:toolkit/mozapps/downloads/tests/unit/xpcshell.ini]
 [include:toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini]
 [include:toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini]
 [include:toolkit/mozapps/update/tests/unit_timermanager/xpcshell.ini]
--- a/toolkit/components/captivedetect/captivedetect.js
+++ b/toolkit/components/captivedetect/captivedetect.js
@@ -5,23 +5,30 @@
 
 'use strict';
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 
+XPCOMUtils.defineLazyServiceGetter(this, "gSysMsgr",
+                                   "@mozilla.org/system-message-internal;1",
+                                   "nsISystemMessagesInternal");
+
 const DEBUG = false; // set to true to show debug messages
 
 const kCAPTIVEPORTALDETECTOR_CONTRACTID = '@mozilla.org/toolkit/captive-detector;1';
 const kCAPTIVEPORTALDETECTOR_CID        = Components.ID('{d9cd00ba-aa4d-47b1-8792-b1fe0cd35060}');
 
 const kOpenCaptivePortalLoginEvent = 'captive-portal-login';
 const kAbortCaptivePortalLoginEvent = 'captive-portal-login-abort';
+const kCaptivePortalLoginSuccessEvent = 'captive-portal-login-success';
+
+const kCaptivePortalSystemMessage = 'captive-portal';
 
 function URLFetcher(url, timeout) {
   let self = this;
   let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
               .createInstance(Ci.nsIXMLHttpRequest);
   xhr.open('GET', url, true);
   // Prevent the request from reading from the cache.
   xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
@@ -327,16 +334,17 @@ CaptivePortalDetector.prototype = {
     let details = {
       type: kOpenCaptivePortalLoginEvent,
       id: id,
       url: this._canonicalSiteURL,
     };
     this._loginObserver.attach();
     this._runningRequest['eventId'] = id;
     this._sendEvent(kOpenCaptivePortalLoginEvent, details);
+    gSysMsgr.broadcastMessage(kCaptivePortalSystemMessage, {});
   },
 
   _mayRetry: function _mayRetry() {
     if (this._runningRequest.retryCount++ < this._maxRetryCount) {
       debug('retry-Detection: ' + this._runningRequest.retryCount + '/' + this._maxRetryCount);
       this._startDetection();
     } else {
       this.executeCallback(true);
@@ -345,16 +353,26 @@ CaptivePortalDetector.prototype = {
 
   executeCallback: function executeCallback(success) {
     if (this._runningRequest) {
       debug('callback executed');
       if (this._runningRequest.hasOwnProperty('callback')) {
         this._runningRequest.callback.complete(success);
       }
 
+      // Only when the request has a event id and |success| is true
+      // do we need to notify the login-success event.
+      if (this._runningRequest.hasOwnProperty('eventId') && success) {
+        let details = {
+          type: kCaptivePortalLoginSuccessEvent,
+          id: this._runningRequest['eventId'],
+        };
+        this._sendEvent(kCaptivePortalLoginSuccessEvent, details);
+      }
+
       // Continue the following request
       this._runningRequest['complete'] = true;
       this._removeRequest(this._runningRequest.interfaceName);
     }
   },
 
   _sendEvent: function _sendEvent(topic, details) {
     debug('sendEvent "' + JSON.stringify(details) + '"');
--- a/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js
+++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found.js
@@ -27,31 +27,40 @@ function fakeUIResponse() {
       let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
                   .createInstance(Ci.nsIXMLHttpRequest);
       xhr.open('GET', gServerURL + kCanonicalSitePath, true);
       xhr.send();
       loginFinished = true;
       do_check_eq(++step, 2);
     }
   }, 'captive-portal-login', false);
+
+  Services.obs.addObserver(function observe(subject, topic, data) {
+    if (topic === 'captive-portal-login-success') {
+      do_check_eq(++step, 4);
+      gServer.stop(do_test_finished);
+    }
+  }, 'captive-portal-login-success', false);
 }
 
 function test_portal_found() {
   do_test_pending();
 
   let callback = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
     prepare: function prepare() {
       do_check_eq(++step, 1);
       gCaptivePortalDetector.finishPreparation(kInterfaceName);
     },
     complete: function complete(success) {
+      // Since this is a synchronous callback, it must happen before
+      // 'captive-portal-login-success' is received.
+      // (Check captivedetect.js::executeCallback
       do_check_eq(++step, 3);
       do_check_true(success);
-      gServer.stop(do_test_finished);
     },
   };
 
   gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
 }
 
 function run_test() {
   run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_found);
--- a/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js
+++ b/toolkit/components/captivedetect/test/unit/test_captive_portal_found_303.js
@@ -29,40 +29,38 @@ function fakeUIResponse() {
       let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
                   .createInstance(Ci.nsIXMLHttpRequest);
       xhr.open('GET', gServerURL + kCanonicalSitePath, true);
       xhr.send();
       loginFinished = true;
       do_check_eq(++step, 2);
     }
   }, 'captive-portal-login', false);
+
+  Services.obs.addObserver(function observe(subject, topic, data) {
+    if (topic === 'captive-portal-login-success') {
+      do_check_eq(++step, 4);
+      gServer.stop(do_test_finished);
+    }
+  }, 'captive-portal-login-success', false);
 }
 
 function test_portal_found() {
   do_test_pending();
 
   let callback = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
     prepare: function prepare() {
       do_check_eq(++step, 1);
       gCaptivePortalDetector.finishPreparation(kInterfaceName);
     },
     complete: function complete(success) {
       do_check_eq(++step, 3);
       do_check_true(success);
-      gServer.stop(do_test_finished);
     },
   };
 
   gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
 }
 
 function run_test() {
   run_captivedetect_test(xhr_handler, fakeUIResponse, test_portal_found);
-
-  server = new HttpServer();
-  server.registerPathHandler(kCanonicalSitePath, xhr_handler);
-  server.start(4444);
-
-  fakeUIResponse();
-
-  test_portal_found();
 }
--- a/toolkit/components/captivedetect/test/unit/test_multiple_requests.js
+++ b/toolkit/components/captivedetect/test/unit/test_multiple_requests.js
@@ -5,16 +5,17 @@
 'use strict';
 
 const kInterfaceName = 'wifi';
 const kOtherInterfaceName = 'ril';
 
 var server;
 var step = 0;
 var loginFinished = false;
+var loginSuccessCount = 0;
 
 function xhr_handler(metadata, response) {
   response.setStatusLine(metadata.httpVersion, 200, 'OK');
   response.setHeader('Cache-Control', 'no-cache', false);
   response.setHeader('Content-Type', 'text/plain', false);
   if (loginFinished) {
     response.write('true');
   } else {
@@ -28,16 +29,26 @@ function fakeUIResponse() {
       let xhr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
                   .createInstance(Ci.nsIXMLHttpRequest);
       xhr.open('GET', gServerURL + kCanonicalSitePath, true);
       xhr.send();
       loginFinished = true;
       do_check_eq(++step, 2);
     }
   }, 'captive-portal-login', false);
+
+  Services.obs.addObserver(function observe(subject, topic, data) {
+    if (topic === 'captive-portal-login-success') {
+      loginSuccessCount++;
+      if (loginSuccessCount > 1) {
+        throw "We should only receive 'captive-portal-login-success' once";
+      }
+      do_check_eq(++step, 4);
+    }
+  }, 'captive-portal-login-success', false);
 }
 
 function test_multiple_requests() {
   do_test_pending();
 
   let callback = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
     prepare: function prepare() {
@@ -48,21 +59,21 @@ function test_multiple_requests() {
       do_check_eq(++step, 3);
       do_check_true(success);
     },
   };
 
   let otherCallback = {
     QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]),
     prepare: function prepare() {
-      do_check_eq(++step, 4);
+      do_check_eq(++step, 5);
       gCaptivePortalDetector.finishPreparation(kOtherInterfaceName);
     },
     complete: function complete(success) {
-      do_check_eq(++step, 5);
+      do_check_eq(++step, 6);
       do_check_true(success);
       gServer.stop(do_test_finished);
     }
   };
 
   gCaptivePortalDetector.checkCaptivePortal(kInterfaceName, callback);
   gCaptivePortalDetector.checkCaptivePortal(kOtherInterfaceName, otherCallback);
 }
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -5,16 +5,17 @@ skip-if = e10s # Bug ?????? - test touch
 [browser_browserDrop.js]
 [browser_bug295977_autoscroll_overflow.js]
 skip-if = e10s # Bug 921935 - focusmanager issues with e10s
 [browser_bug594509.js]
 skip-if = e10s # Bug ?????? - intermittent crash of child process reported when run under e10s
 [browser_bug982298.js]
 [browser_default_image_filename.js]
 skip-if = e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
+[browser_f7_caret_browsing.js]
 [browser_findbar.js]
 skip-if = e10s # Disabled for e10s: Bug ?????? - seems to be a timing issue with RemoteFinder.jsm messages coming later than the tests expect.
 [browser_input_file_tooltips.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (TypeError: doc.createElement is not a function)
 [browser_keyevents_during_autoscrolling.js]
 skip-if = e10s # Bug 921935 - focusmanager issues with e10s
 [browser_save_resend_postdata.js]
 support-files =
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_f7_caret_browsing.js
@@ -0,0 +1,242 @@
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
+let gTab = null;
+let gListener = null;
+const kURL = "data:text/html;charset=utf-8,Caret browsing is fun.<input id='in'>";
+
+const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
+const kPrefWarnOnEnable    = "accessibility.warn_on_browsewithcaret";
+const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
+
+let oldPrefs = {};
+for (let pref of [kPrefShortcutEnabled, kPrefWarnOnEnable, kPrefCaretBrowsingOn]) {
+  oldPrefs[pref] = Services.prefs.getBoolPref(pref);
+}
+
+Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+registerCleanupFunction(function() {
+  if (gTab)
+    gBrowser.removeTab(gTab);
+  if (gListener)
+    Services.wm.removeListener(gListener);
+
+  for (let pref of [kPrefShortcutEnabled, kPrefWarnOnEnable, kPrefCaretBrowsingOn]) {
+    Services.prefs.setBoolPref(pref, oldPrefs[pref]);
+  }
+});
+
+function promiseWaitForFocusEvent(el) {
+  let deferred = Promise.defer();
+  el.addEventListener("focus", function listener() {
+    el.removeEventListener("focus", listener, false);
+    deferred.resolve();
+  }, false);
+  return deferred.promise;
+}
+
+function promiseTestPageLoad() {
+  let deferred = Promise.defer();
+  info("Waiting for test page to load.");
+
+  gTab = gBrowser.selectedTab = gBrowser.addTab(kURL);
+  let browser = gBrowser.selectedBrowser;
+  browser.addEventListener("load", function listener() {
+    if (browser.currentURI.spec == "about:blank")
+      return;
+    info("Page loaded: " + browser.currentURI.spec);
+    browser.removeEventListener("load", listener, true);
+
+    deferred.resolve();
+  }, true);
+
+  return deferred.promise;
+}
+
+function promiseCaretPromptOpened() {
+  let deferred = Promise.defer();
+  if (gListener) {
+    console.trace();
+    ok(false, "Should not be waiting for another prompt right now.");
+    return false;
+  }
+  info("Waiting for caret prompt to open");
+  gListener = {
+    onOpenWindow: function(win) {
+      let window = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                      .getInterface(Ci.nsIDOMWindow);
+      window.addEventListener("load", function listener() {
+        window.removeEventListener("load", listener);
+        if (window.location.href == "chrome://global/content/commonDialog.xul") {
+          info("Caret prompt opened, removing listener and focusing");
+          Services.wm.removeListener(gListener);
+          gListener = null;
+          deferred.resolve(window);
+        }
+      });
+    },
+    onCloseWindow: function() {},
+  };
+  Services.wm.addListener(gListener);
+  return deferred.promise;
+}
+
+function hitF7(async = true) {
+  let f7 = () => EventUtils.sendKey("F7", window.content);
+  // Need to not stop execution inside this task:
+  if (async) {
+    executeSoon(f7);
+  } else {
+    f7();
+  }
+}
+
+function syncToggleCaretNoDialog(expected) {
+  let openedDialog = false;
+  promiseCaretPromptOpened().then(function(win) {
+    openedDialog = true;
+    win.close(); // This will eventually return focus here and allow the test to continue...
+  });
+  // Cause the dialog to appear sync, if it still does.
+  hitF7(false);
+  if (gListener) {
+    Services.wm.removeListener(gListener);
+    gListener = null;
+  }
+  let expectedStr = expected ? "on." : "off.";
+  ok(!openedDialog, "Shouldn't open a dialog to turn caret browsing " + expectedStr);
+  let prefVal = Services.prefs.getBoolPref(kPrefCaretBrowsingOn);
+  is(prefVal, expected, "Caret browsing should now be " + expectedStr);
+}
+
+add_task(function* checkTogglingCaretBrowsing() {
+  yield promiseTestPageLoad();
+  let textEl = window.content.document.getElementById("in");
+  textEl.focus();
+
+  let promiseGotKey = promiseCaretPromptOpened();
+  hitF7();
+  let prompt = yield promiseGotKey;
+  let doc = prompt.document;
+  is(doc.documentElement.defaultButton, "cancel", "'No' button should be the default");
+  ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default.");
+  let promiseInputFocused = promiseWaitForFocusEvent(textEl);
+  doc.documentElement.cancelDialog();
+  yield promiseInputFocused;
+  ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off after cancelling the dialog.");
+
+  promiseGotKey = promiseCaretPromptOpened();
+  hitF7();
+  prompt = yield promiseGotKey;
+
+  doc = prompt.document;
+  is(doc.documentElement.defaultButton, "cancel", "'No' button should be the default");
+  ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default.");
+  promiseInputFocused = promiseWaitForFocusEvent(textEl);
+  doc.documentElement.acceptDialog();
+  yield promiseInputFocused;
+  ok(Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should be on after accepting the dialog.");
+
+  syncToggleCaretNoDialog(false);
+
+  promiseGotKey = promiseCaretPromptOpened();
+  hitF7();
+  prompt = yield promiseGotKey;
+  doc = prompt.document;
+
+  is(doc.documentElement.defaultButton, "cancel", "'No' button should be the default");
+  ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default.");
+
+  promiseInputFocused = promiseWaitForFocusEvent(textEl);
+  doc.documentElement.cancelDialog();
+  yield promiseInputFocused;
+
+  ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off after cancelling the dialog.");
+
+  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+  gBrowser.removeTab(gTab);
+  gTab = null;
+});
+
+add_task(function* toggleCheckboxNoCaretBrowsing() {
+  yield promiseTestPageLoad();
+  let textEl = window.content.document.getElementById("in");
+  textEl.focus();
+
+  let promiseGotKey = promiseCaretPromptOpened();
+  hitF7();
+  let prompt = yield promiseGotKey;
+  let doc = prompt.document;
+  is(doc.documentElement.defaultButton, "cancel", "'No' button should be the default");
+  let checkbox = doc.getElementById("checkbox");
+  ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");
+
+  // Check the box:
+  checkbox.click();
+  let promiseInputFocused = promiseWaitForFocusEvent(textEl);
+  // Say no:
+  doc.documentElement.getButton("cancel").click();
+  yield promiseInputFocused;
+  ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off.");
+
+  ok(!Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should now be disabled.");
+
+  syncToggleCaretNoDialog(false);
+  ok(!Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should still be disabled.");
+
+  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+  gBrowser.removeTab(gTab);
+  gTab = null;
+});
+
+
+add_task(function* toggleCheckboxNoCaretBrowsing() {
+  yield promiseTestPageLoad();
+  let textEl = window.content.document.getElementById("in");
+  textEl.focus();
+
+  let promiseGotKey = promiseCaretPromptOpened();
+  hitF7();
+  let prompt = yield promiseGotKey;
+  let doc = prompt.document;
+  is(doc.documentElement.defaultButton, "cancel", "'No' button should be the default");
+  let checkbox = doc.getElementById("checkbox");
+  ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");
+
+  // Check the box:
+  checkbox.click();
+  let promiseInputFocused = promiseWaitForFocusEvent(textEl);
+  // Say yes:
+  doc.documentElement.acceptDialog();
+  yield promiseInputFocused;
+  ok(Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should now be on.");
+  ok(Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should still be enabled.");
+  ok(!Services.prefs.getBoolPref(kPrefWarnOnEnable), "Should no longer warn when enabling.");
+
+
+  syncToggleCaretNoDialog(false);
+  syncToggleCaretNoDialog(true);
+  syncToggleCaretNoDialog(false);
+  
+  Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
+  Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
+  Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
+
+  gBrowser.removeTab(gTab);
+  gTab = null;
+});
+
+
+
+
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -1095,58 +1095,70 @@
     </implementation>
 
     <handlers>
       <handler event="keypress" keycode="VK_F7" group="system">
         <![CDATA[
           if (event.defaultPrevented || !event.isTrusted)
             return;
 
-          var isEnabled = this.mPrefs.getBoolPref("accessibility.browsewithcaret_shortcut.enabled");
+          const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
+          const kPrefWarnOnEnable    = "accessibility.warn_on_browsewithcaret";
+          const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
+
+          var isEnabled = this.mPrefs.getBoolPref(kPrefShortcutEnabled);
           if (!isEnabled)
             return;
 
           // Toggle browse with caret mode
           var browseWithCaretOn = false;
           var warn = true;
 
           try {
-            warn = this.mPrefs.getBoolPref("accessibility.warn_on_browsewithcaret");
+            warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable);
           } catch (ex) {
           }
 
           try {
-            browseWithCaretOn = this.mPrefs.getBoolPref("accessibility.browsewithcaret");
+            browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn);
           } catch (ex) {
           }
           if (warn && !browseWithCaretOn) {
             var checkValue = {value:false};
             var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                                           .getService(Components.interfaces.nsIPromptService);
 
             var buttonPressed = promptService.confirmEx(window,
               this.mStrBundle.GetStringFromName('browsewithcaret.checkWindowTitle'),
               this.mStrBundle.GetStringFromName('browsewithcaret.checkLabel'),
-              promptService.STD_YES_NO_BUTTONS,
+              // Make "No" the default:
+              promptService.STD_YES_NO_BUTTONS | promptService.BUTTON_POS_1_DEFAULT,
               null, null, null, this.mStrBundle.GetStringFromName('browsewithcaret.checkMsg'),
               checkValue);
-            if (buttonPressed != 0)
+            if (buttonPressed != 0) {
+              if (checkValue.value) {
+                try {
+                  this.mPrefs.setBoolPref(kPrefShortcutEnabled, false);
+                } catch (ex) {
+                }
+              }
               return;
+            }
             if (checkValue.value) {
               try {
-                this.mPrefs.setBoolPref("accessibility.warn_on_browsewithcaret", false);
+                this.mPrefs.setBoolPref(kPrefWarnOnEnable, false);
               }
               catch (ex) {
               }
             }
           }
 
           // Toggle the pref
           try {
-            this.mPrefs.setBoolPref("accessibility.browsewithcaret",!browseWithCaretOn);
+            this.mPrefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
           } catch (ex) {
           }
         ]]>
       </handler>
       <handler event="dragover" group="system">
       <![CDATA[
         if (!this.droppedLinkHandler || event.defaultPrevented)
           return;
--- a/toolkit/modules/DirectoryLinksProvider.jsm
+++ b/toolkit/modules/DirectoryLinksProvider.jsm
@@ -9,35 +9,39 @@ this.EXPORTED_SYMBOLS = ["DirectoryLinks
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const XMLHttpRequest =
   Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIXMLHttpRequest");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
+  return new TextDecoder();
+});
 
 // The filename where directory links are stored locally
 const DIRECTORY_LINKS_FILE = "directoryLinks.json";
 
 // The preference that tells whether to match the OS locale
 const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
 
 // The preference that tells what locale the user selected
 const PREF_SELECTED_LOCALE = "general.useragent.locale";
 
 // The preference that tells where to obtain directory links
-const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directorySource";
+const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directory.source";
 
 // The frecency of a directory link
 const DIRECTORY_FRECENCY = 1000;
 
 const LINK_TYPES = Object.freeze([
   "sponsored",
   "affiliate",
   "organic",
@@ -47,17 +51,23 @@ const LINK_TYPES = Object.freeze([
  * Singleton that serves as the provider of directory links.
  * Directory links are a hard-coded set of links shown if a user's link
  * inventory is empty.
  */
 let DirectoryLinksProvider = {
 
   __linksURL: null,
 
-  _observers: [],
+  _observers: new Set(),
+
+  // links download deferred, resolved upon download completion
+  _downloadDeferred: null,
+
+  // download default interval is 24 hours in milliseconds
+  _downloadIntervalMS: 86400000,
 
   get _observedPrefs() Object.freeze({
     linksURL: PREF_DIRECTORY_SOURCE,
     matchOSLocale: PREF_MATCH_OS_LOCALE,
     prefSelectedLocale: PREF_SELECTED_LOCALE,
   }),
 
   get _linksURL() {
@@ -106,82 +116,49 @@ let DirectoryLinksProvider = {
 
   get linkTypes() LINK_TYPES,
 
   observe: function DirectoryLinksProvider_observe(aSubject, aTopic, aData) {
     if (aTopic == "nsPref:changed") {
       if (aData == this._observedPrefs["linksURL"]) {
         delete this.__linksURL;
       }
-      this._callObservers("onManyLinksChanged");
     }
+    // force directory download on changes to any of the observed prefs
+    this._fetchAndCacheLinksIfNecessary(true);
   },
 
   _addPrefsObserver: function DirectoryLinksProvider_addObserver() {
     for (let pref in this._observedPrefs) {
       let prefName = this._observedPrefs[pref];
       Services.prefs.addObserver(prefName, this, false);
     }
   },
 
   _removePrefsObserver: function DirectoryLinksProvider_removeObserver() {
     for (let pref in this._observedPrefs) {
       let prefName = this._observedPrefs[pref];
       Services.prefs.removeObserver(prefName, this);
     }
   },
 
-  /**
-   * Fetches the current set of directory links.
-   * @param aCallback a callback that is provided a set of links.
-   */
-  _fetchLinks: function DirectoryLinksProvider_fetchLinks(aCallback) {
-    try {
-      NetUtil.asyncFetch(this._linksURL, (aInputStream, aResult, aRequest) => {
-        let output;
-        if (Components.isSuccessCode(aResult)) {
-          try {
-            let json = NetUtil.readInputStreamToString(aInputStream,
-                                                       aInputStream.available(),
-                                                       {charset: "UTF-8"});
-            let locale = this.locale;
-            output = JSON.parse(json)[locale];
-          }
-          catch (e) {
-            Cu.reportError(e);
-          }
-        }
-        else {
-          Cu.reportError(new Error("the fetch of " + this._linksURL + "was unsuccessful"));
-        }
-        aCallback(output || []);
-      });
-    }
-    catch (e) {
-      Cu.reportError(e);
-      aCallback([]);
-    }
-  },
-
   _fetchAndCacheLinks: function DirectoryLinksProvider_fetchAndCacheLinks(uri) {
     let deferred = Promise.defer();
     let xmlHttp = new XMLHttpRequest();
     xmlHttp.overrideMimeType("application/json");
 
     let self = this;
     xmlHttp.onload = function(aResponse) {
       let json = this.responseText;
       if (this.status && this.status != 200) {
         json = "{}";
       }
-      let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, DIRECTORY_LINKS_FILE);
-      OS.File.writeAtomic(directoryLinksFilePath, json, {tmpPath: directoryLinksFilePath + ".tmp"})
+      OS.File.writeAtomic(self._directoryFilePath, json, {tmpPath: self._directoryFilePath + ".tmp"})
         .then(() => {
           deferred.resolve();
-          self._callObservers("onManyLinksChanged");
         },
         () => {
           deferred.reject("Error writing uri data in profD.");
         });
     };
 
     xmlHttp.onerror = function(e) {
       deferred.reject("Fetching " + uri + " results in error code: " + e.target.status);
@@ -193,57 +170,153 @@ let DirectoryLinksProvider = {
     } catch (e) {
       deferred.reject("Error fetching " + uri);
       Cu.reportError(e);
     }
     return deferred.promise;
   },
 
   /**
+   * Downloads directory links if needed
+   * @return promise resolved immediately if no download needed, or upon completion
+   */
+  _fetchAndCacheLinksIfNecessary: function DirectoryLinksProvider_fetchAndCacheLinksIfNecessary(forceDownload=false) {
+    if (this._downloadDeferred) {
+      // fetching links already - just return the promise
+      return this._downloadDeferred.promise;
+    }
+
+    if (forceDownload || this._needsDownload) {
+      this._downloadDeferred = Promise.defer();
+      this._fetchAndCacheLinks(this._linksURL).then(() => {
+        // the new file was successfully downloaded and cached, so update a timestamp
+        this._lastDownloadMS = Date.now();
+        this._downloadDeferred.resolve();
+        this._downloadDeferred = null;
+        this._callObservers("onManyLinksChanged")
+      },
+      error => {
+        this._downloadDeferred.resolve();
+        this._downloadDeferred = null;
+        this._callObservers("onDownloadFail");
+      });
+      return this._downloadDeferred.promise;
+    }
+
+    // download is not needed
+    return Promise.resolve();
+  },
+
+  /**
+   * @return true if download is needed, false otherwise
+   */
+  get _needsDownload () {
+    // fail if last download occured less then 24 hours ago
+    if ((Date.now() - this._lastDownloadMS) > this._downloadIntervalMS) {
+      return true;
+    }
+    return false;
+  },
+
+  /**
+   * Reads directory links file and parses its content
+   * @return a promise resolved to valid list of links or [] if read or parse fails
+   */
+  _readDirectoryLinksFile: function DirectoryLinksProvider_readDirectoryLinksFile() {
+    return OS.File.read(this._directoryFilePath).then(binaryData => {
+      let output;
+      try {
+        let locale = this.locale;
+        let json = gTextDecoder.decode(binaryData);
+        output = JSON.parse(json)[locale];
+      }
+      catch (e) {
+        Cu.reportError(e);
+      }
+      return output || [];
+    },
+    error => {
+      Cu.reportError(error);
+      return [];
+    });
+  },
+
+  /**
+   * Submits counts of shown directory links for each type and
+   * triggers directory download if sponsored link was shown
+   *
+   * @param object keyed on types containing counts
+   * @return download promise
+   */
+  reportShownCount: function DirectoryLinksProvider_reportShownCount(directoryCount) {
+    if (directoryCount.sponsored > 0
+        || directoryCount.affiliate > 0
+        || directoryCount.organic > 0) {
+      return this._fetchAndCacheLinksIfNecessary();
+    }
+    return Promise.resolve();
+  },
+
+  /**
    * Gets the current set of directory links.
    * @param aCallback The function that the array of links is passed to.
    */
   getLinks: function DirectoryLinksProvider_getLinks(aCallback) {
-    this._fetchLinks(rawLinks => {
+    this._readDirectoryLinksFile().then(rawLinks => {
       // all directory links have a frecency of DIRECTORY_FRECENCY
       aCallback(rawLinks.map((link, position) => {
         link.frecency = DIRECTORY_FRECENCY;
         link.lastVisitDate = rawLinks.length - position;
         return link;
       }));
     });
   },
 
   init: function DirectoryLinksProvider_init() {
     this._addPrefsObserver();
+    // setup directory file path and last download timestamp
+    this._directoryFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, DIRECTORY_LINKS_FILE);
+    this._lastDownloadMS = 0;
+    return Task.spawn(function() {
+      // get the last modified time of the links file if it exists
+      let doesFileExists = yield OS.File.exists(this._directoryFilePath);
+      if (doesFileExists) {
+        let fileInfo = yield OS.File.stat(this._directoryFilePath);
+        this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate);
+      }
+      // fetch directory on startup without force
+      yield this._fetchAndCacheLinksIfNecessary();
+    }.bind(this));
   },
 
   /**
    * Return the object to its pre-init state
    */
   reset: function DirectoryLinksProvider_reset() {
     delete this.__linksURL;
     this._removePrefsObserver();
     this._removeObservers();
   },
 
   addObserver: function DirectoryLinksProvider_addObserver(aObserver) {
-    this._observers.push(aObserver);
+    this._observers.add(aObserver);
+  },
+
+  removeObserver: function DirectoryLinksProvider_removeObserver(aObserver) {
+    this._observers.delete(aObserver);
   },
 
   _callObservers: function DirectoryLinksProvider__callObservers(aMethodName, aArg) {
     for (let obs of this._observers) {
       if (typeof(obs[aMethodName]) == "function") {
         try {
           obs[aMethodName](this, aArg);
         } catch (err) {
           Cu.reportError(err);
         }
       }
     }
   },
 
   _removeObservers: function() {
-    while (this._observers.length) {
-      this._observers.pop();
-    }
+    this._observers.clear();
   }
 };
--- a/toolkit/modules/tests/xpcshell/test_DirectoryLinksProvider.js
+++ b/toolkit/modules/tests/xpcshell/test_DirectoryLinksProvider.js
@@ -9,31 +9,37 @@
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu, Constructor: CC } = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Http.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://gre/modules/osfile.jsm")
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 
 do_get_profile();
 
 const DIRECTORY_LINKS_FILE = "directoryLinks.json";
 const DIRECTORY_FRECENCY = 1000;
 const kURLData = {"en-US": [{"url":"http://example.com","title":"LocalSource"}]};
 const kTestURL = 'data:application/json,' + JSON.stringify(kURLData);
 
 // DirectoryLinksProvider preferences
 const kLocalePref = DirectoryLinksProvider._observedPrefs.prefSelectedLocale;
 const kSourceUrlPref = DirectoryLinksProvider._observedPrefs.linksURL;
 
+// app/profile/firefox.js are not avaialble in xpcshell: hence, preset them
+Services.prefs.setCharPref(kLocalePref, "en-US");
+Services.prefs.setCharPref(kSourceUrlPref, kTestURL);
+
 // httpd settings
 var server;
 const kDefaultServerPort = 9000;
 const kBaseUrl = "http://localhost:" + kDefaultServerPort;
 const kExamplePath = "/exampleTest/";
 const kFailPath = "/fail/";
 const kExampleURL = kBaseUrl + kExamplePath;
 const kFailURL = kBaseUrl + kFailPath;
@@ -98,172 +104,328 @@ function readJsonFile(jsonFile = DIRECTO
   }, () => { return "" });
 }
 
 function cleanJsonFile(jsonFile = DIRECTORY_LINKS_FILE) {
   let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile);
   return OS.File.remove(directoryLinksFilePath);
 }
 
-// All tests that call setupDirectoryLinksProvider() must also call cleanDirectoryLinksProvider().
-function setupDirectoryLinksProvider(options = {}) {
-  let linksURL = options.linksURL || kTestURL;
-  DirectoryLinksProvider.init();
-  Services.prefs.setCharPref(kLocalePref, options.locale || "en-US");
-  Services.prefs.setCharPref(kSourceUrlPref, linksURL);
-  do_check_eq(DirectoryLinksProvider._linksURL, linksURL);
+function LinksChangeObserver() {
+  this.deferred = Promise.defer();
+  this.onManyLinksChanged = () => this.deferred.resolve();
+  this.onDownloadFail = this.onManyLinksChanged;
 }
 
-function cleanDirectoryLinksProvider() {
-  DirectoryLinksProvider.reset();
-  Services.prefs.clearUserPref(kLocalePref);
-  Services.prefs.clearUserPref(kSourceUrlPref);
+function promiseDirectoryDownloadOnPrefChange(pref, newValue) {
+  let oldValue = Services.prefs.getCharPref(pref);
+  if (oldValue != newValue) {
+    // if the preference value is already equal to newValue
+    // the pref service will not call our observer and we
+    // deadlock. Hence only setup observer if values differ
+    let observer = new LinksChangeObserver();
+    DirectoryLinksProvider.addObserver(observer);
+    Services.prefs.setCharPref(pref, newValue);
+    return observer.deferred.promise;
+  }
+  return Promise.resolve();
+}
+
+function promiseSetupDirectoryLinksProvider(options = {}) {
+  return Task.spawn(function() {
+    let linksURL = options.linksURL || kTestURL;
+    yield DirectoryLinksProvider.init();
+    yield promiseDirectoryDownloadOnPrefChange(kLocalePref, options.locale || "en-US");
+    yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, linksURL);
+    do_check_eq(DirectoryLinksProvider._linksURL, linksURL);
+    DirectoryLinksProvider._lastDownloadMS = options.lastDownloadMS || 0;
+  });
+}
+
+function promiseCleanDirectoryLinksProvider() {
+  return Task.spawn(function() {
+    yield promiseDirectoryDownloadOnPrefChange(kLocalePref, "en-US");
+    yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kTestURL);
+    DirectoryLinksProvider._lastDownloadMS  = 0;
+    DirectoryLinksProvider.reset();
+  });
 }
 
 function run_test() {
   // Set up a mock HTTP server to serve a directory page
   server = new HttpServer();
   server.registerPrefixHandler(kExamplePath, getHttpHandler(kExamplePath));
   server.registerPrefixHandler(kFailPath, getHttpHandler(kFailPath));
   server.start(kDefaultServerPort);
 
   run_next_test();
 
   // Teardown.
   do_register_cleanup(function() {
     server.stop(function() { });
+    DirectoryLinksProvider.reset();
+    Services.prefs.clearUserPref(kLocalePref);
+    Services.prefs.clearUserPref(kSourceUrlPref);
   });
 }
 
 add_task(function test_fetchAndCacheLinks_local() {
+  yield DirectoryLinksProvider.init();
   yield cleanJsonFile();
   // Trigger cache of data or chrome uri files in profD
   yield DirectoryLinksProvider._fetchAndCacheLinks(kTestURL);
   let data = yield readJsonFile();
   isIdentical(data, kURLData);
 });
 
 add_task(function test_fetchAndCacheLinks_remote() {
+  yield DirectoryLinksProvider.init();
   yield cleanJsonFile();
   // this must trigger directory links json download and save it to cache file
   yield DirectoryLinksProvider._fetchAndCacheLinks(kExampleURL);
   let data = yield readJsonFile();
   isIdentical(data, kHttpHandlerData[kExamplePath]);
 });
 
 add_task(function test_fetchAndCacheLinks_malformedURI() {
+  yield DirectoryLinksProvider.init();
   yield cleanJsonFile();
   let someJunk = "some junk";
   try {
     yield DirectoryLinksProvider._fetchAndCacheLinks(someJunk);
     do_throw("Malformed URIs should fail")
   } catch (e) {
     do_check_eq(e, "Error fetching " + someJunk)
   }
 
   // File should be empty.
   let data = yield readJsonFile();
   isIdentical(data, "");
 });
 
 add_task(function test_fetchAndCacheLinks_unknownHost() {
+  yield DirectoryLinksProvider.init();
   yield cleanJsonFile();
   let nonExistentServer = "http://nosuchhost";
   try {
     yield DirectoryLinksProvider._fetchAndCacheLinks(nonExistentServer);
     do_throw("BAD URIs should fail");
   } catch (e) {
     do_check_true(e.startsWith("Fetching " + nonExistentServer + " results in error code: "))
   }
 
   // File should be empty.
   let data = yield readJsonFile();
   isIdentical(data, "");
 });
 
 add_task(function test_fetchAndCacheLinks_non200Status() {
+  yield DirectoryLinksProvider.init();
   yield cleanJsonFile();
   yield DirectoryLinksProvider._fetchAndCacheLinks(kFailURL);
   let data = yield readJsonFile();
   isIdentical(data, {});
 });
 
 // To test onManyLinksChanged observer, trigger a fetch
-add_task(function test_linkObservers() {
-  let deferred = Promise.defer();
-  let testObserver = {
-    onManyLinksChanged: function() {
-      deferred.resolve();
-    }
-  }
+add_task(function test_DirectoryLinksProvider__linkObservers() {
+  yield DirectoryLinksProvider.init();
 
-  DirectoryLinksProvider.init();
+  let testObserver = new LinksChangeObserver();
   DirectoryLinksProvider.addObserver(testObserver);
-  do_check_eq(DirectoryLinksProvider._observers.length, 1);
-  DirectoryLinksProvider._fetchAndCacheLinks(kTestURL);
+  do_check_eq(DirectoryLinksProvider._observers.size, 1);
+  DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true);
 
-  yield deferred.promise;
+  yield testObserver.deferred.promise;
   DirectoryLinksProvider._removeObservers();
-  do_check_eq(DirectoryLinksProvider._observers.length, 0);
+  do_check_eq(DirectoryLinksProvider._observers.size, 0);
 
-  cleanDirectoryLinksProvider();
+  yield promiseCleanDirectoryLinksProvider();
 });
 
 add_task(function test_linksURL_locale() {
   let data = {
     "en-US": [{url: "http://example.com", title: "US"}],
     "zh-CN": [
               {url: "http://example.net", title: "CN"},
               {url:"http://example.net/2", title: "CN2"}
     ],
   };
   let dataURI = 'data:application/json,' + JSON.stringify(data);
 
-  setupDirectoryLinksProvider({linksURL: dataURI});
+  yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
 
   let links;
   let expected_data;
 
   links = yield fetchData();
   do_check_eq(links.length, 1);
   expected_data = [{url: "http://example.com", title: "US", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}];
   isIdentical(links, expected_data);
 
-  Services.prefs.setCharPref('general.useragent.locale', 'zh-CN');
+  yield promiseDirectoryDownloadOnPrefChange("general.useragent.locale", "zh-CN");
 
   links = yield fetchData();
   do_check_eq(links.length, 2)
   expected_data = [
     {url: "http://example.net", title: "CN", frecency: DIRECTORY_FRECENCY, lastVisitDate: 2},
     {url: "http://example.net/2", title: "CN2", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}
   ];
   isIdentical(links, expected_data);
 
-  cleanDirectoryLinksProvider();
+  yield promiseCleanDirectoryLinksProvider();
 });
 
-add_task(function test_prefObserver_url() {
-  setupDirectoryLinksProvider({linksURL: kTestURL});
+add_task(function test_DirectoryLinksProvider__prefObserver_url() {
+  yield promiseSetupDirectoryLinksProvider({linksURL: kTestURL});
 
   let links = yield fetchData();
   do_check_eq(links.length, 1);
   let expectedData =  [{url: "http://example.com", title: "LocalSource", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}];
   isIdentical(links, expectedData);
 
   // tests these 2 things:
   // 1. _linksURL is properly set after the pref change
   // 2. invalid source url is correctly handled
   let exampleUrl = 'http://nosuchhost/bad';
-  Services.prefs.setCharPref(kSourceUrlPref, exampleUrl);
+  yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl);
   do_check_eq(DirectoryLinksProvider._linksURL, exampleUrl);
 
+  // since the download fail, the directory file must remain the same
   let newLinks = yield fetchData();
+  isIdentical(newLinks, expectedData);
+
+  // now remove the file, and re-download
+  yield cleanJsonFile();
+  yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl + " ");
+  // we now should see empty links
+  newLinks = yield fetchData();
   isIdentical(newLinks, []);
 
-  cleanDirectoryLinksProvider();
+  yield promiseCleanDirectoryLinksProvider();
+});
+
+add_task(function test_DirectoryLinksProvider_getLinks_noLocaleData() {
+  yield promiseSetupDirectoryLinksProvider({locale: 'zh-CN'});
+  let links = yield fetchData();
+  do_check_eq(links.length, 0);
+  yield promiseCleanDirectoryLinksProvider();
+});
+
+add_task(function test_DirectoryLinksProvider_needsDownload() {
+  // test timestamping
+  DirectoryLinksProvider._lastDownloadMS = 0;
+  do_check_true(DirectoryLinksProvider._needsDownload);
+  DirectoryLinksProvider._lastDownloadMS = Date.now();
+  do_check_false(DirectoryLinksProvider._needsDownload);
+  DirectoryLinksProvider._lastDownloadMS = Date.now() - (60*60*24 + 1)*1000;
+  do_check_true(DirectoryLinksProvider._needsDownload);
+  DirectoryLinksProvider._lastDownloadMS = 0;
+});
+
+add_task(function test_DirectoryLinksProvider_fetchAndCacheLinksIfNecessary() {
+  yield DirectoryLinksProvider.init();
+  yield cleanJsonFile();
+  // explicitly change source url to cause the download during setup
+  yield promiseSetupDirectoryLinksProvider({linksURL: kTestURL+" "});
+  yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary();
+
+  // inspect lastDownloadMS timestamp which should be 5 seconds less then now()
+  let lastDownloadMS = DirectoryLinksProvider._lastDownloadMS;
+  do_check_true((Date.now() - lastDownloadMS) < 5000);
+
+  // we should have fetched a new file during setup
+  let data = yield readJsonFile();
+  isIdentical(data, kURLData);
+
+  // attempt to download again - the timestamp should not change
+  yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary();
+  do_check_eq(DirectoryLinksProvider._lastDownloadMS, lastDownloadMS);
+
+  // clean the file and force the download
+  yield cleanJsonFile();
+  yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true);
+  data = yield readJsonFile();
+  isIdentical(data, kURLData);
+
+  // make sure that failed download does not corrupt the file, nor changes lastDownloadMS
+  lastDownloadMS = DirectoryLinksProvider._lastDownloadMS;
+  yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, "http://");
+  yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true);
+  data = yield readJsonFile();
+  isIdentical(data, kURLData);
+  do_check_eq(DirectoryLinksProvider._lastDownloadMS, lastDownloadMS);
+
+  // _fetchAndCacheLinksIfNecessary must return same promise if download is in progress
+  let downloadPromise = DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true);
+  let anotherPromise = DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true);
+  do_check_true(downloadPromise === anotherPromise);
+  yield downloadPromise;
+
+  yield promiseCleanDirectoryLinksProvider();
 });
 
-add_task(function test_getLinks_noLocaleData() {
-  setupDirectoryLinksProvider({locale: 'zh-CN'});
-  let links = yield fetchData();
-  do_check_eq(links.length, 0);
-  cleanDirectoryLinksProvider();
+add_task(function test_DirectoryLinksProvider_fetchDirectoryOnPrefChange() {
+  yield DirectoryLinksProvider.init();
+
+  let testObserver = new LinksChangeObserver();
+  DirectoryLinksProvider.addObserver(testObserver);
+
+  yield cleanJsonFile();
+  // ensure that provider does not think it needs to download
+  do_check_false(DirectoryLinksProvider._needsDownload);
+
+  // change the source URL, which should force directory download
+  yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL);
+  // then wait for testObserver to fire and test that json is downloaded
+  yield testObserver.deferred.promise;
+  let data = yield readJsonFile();
+  isIdentical(data, kHttpHandlerData[kExamplePath]);
+
+  yield promiseCleanDirectoryLinksProvider();
 });
+
+add_task(function test_DirectoryLinksProvider_fetchDirectoryOnShowCount() {
+  yield promiseSetupDirectoryLinksProvider();
+
+  // set lastdownload to 0 to make DirectoryLinksProvider want to download
+  DirectoryLinksProvider._lastDownloadMS = 0;
+  do_check_true(DirectoryLinksProvider._needsDownload);
+
+  // Tell DirectoryLinksProvider that newtab has no room for sponsored links
+  let directoryCount = {sponsored: 0};
+  yield DirectoryLinksProvider.reportShownCount(directoryCount);
+  // the provider must skip download, hence that lastdownload is still 0
+  do_check_eq(DirectoryLinksProvider._lastDownloadMS, 0);
+
+  // make room for sponsored links and repeat, download should happen
+  directoryCount.sponsored = 1;
+  yield DirectoryLinksProvider.reportShownCount(directoryCount);
+  do_check_true(DirectoryLinksProvider._lastDownloadMS != 0);
+
+  yield promiseCleanDirectoryLinksProvider();
+});
+
+add_task(function test_DirectoryLinksProvider_fetchDirectoryOnInit() {
+  // ensure preferences are set to defaults
+  yield promiseSetupDirectoryLinksProvider();
+  // now clean to provider, so we can init it again
+  yield promiseCleanDirectoryLinksProvider();
+
+  yield cleanJsonFile();
+  yield DirectoryLinksProvider.init();
+  let data = yield readJsonFile();
+  isIdentical(data, kURLData);
+
+  yield promiseCleanDirectoryLinksProvider();
+});
+
+add_task(function test_DirectoryLinksProvider_getLinksFromCorruptedFile() {
+  yield promiseSetupDirectoryLinksProvider();
+
+  // write bogus json to a file and attempt to fetch from it
+  let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.profileDir, DIRECTORY_LINKS_FILE);
+  yield OS.File.writeAtomic(directoryLinksFilePath, '{"en-US":');
+  let data = yield fetchData();
+  isIdentical(data, []);
+
+  yield promiseCleanDirectoryLinksProvider();
+});