Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 02 Jul 2014 15:09:37 +0200
changeset 191902 60133a85f8aedb4b1bf05f3fbe4ff28c62e1bf02
parent 191854 924708eb83751ccabf9298d62a89cc684db5a43d (current diff)
parent 191901 e82a9700f94b386bed26a45704a4177a3f251141 (diff)
child 191903 6463e6d90decbd1b733a74805e5f3363a8e82c5a
push id45685
push usercbook@mozilla.com
push dateWed, 02 Jul 2014 13:09:48 +0000
treeherdermozilla-inbound@60133a85f8ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone33.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
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -474,16 +474,21 @@ pref("services.push.udp.wakeupEnabled", 
 pref("services.push.udp.well-known_netidAddress", "_wakeup_.");
 
 // NetworkStats
 #ifdef MOZ_WIDGET_GONK
 pref("dom.mozNetworkStats.enabled", true);
 pref("dom.webapps.firstRunWithSIM", true);
 #endif
 
+// ResourceStats
+#ifdef MOZ_WIDGET_GONK
+pref("dom.resource_stats.enabled", true);
+#endif
+
 #ifdef MOZ_B2G_RIL
 // SingleVariant
 pref("dom.mozApps.single_variant_sourcedir", "/persist/svoperapps");
 #endif
 
 // WebSettings
 pref("dom.mozSettings.enabled", true);
 pref("dom.navigator-property.disable.mozSettings", false);
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -14,16 +14,17 @@ Cu.import('resource://gre/modules/Notifi
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
 Cu.import('resource://gre/modules/Keyboard.jsm');
 Cu.import('resource://gre/modules/ErrorPage.jsm');
 Cu.import('resource://gre/modules/AlertsHelper.jsm');
 #ifdef MOZ_WIDGET_GONK
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
+Cu.import('resource://gre/modules/ResourceStatsService.jsm');
 #endif
 
 // Identity
 Cu.import('resource://gre/modules/SignInToWebsite.jsm');
 SignInToWebsiteController.init();
 
 #ifdef MOZ_SERVICES_FXACCOUNTS
 Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,18 +14,18 @@
   <!--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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="3326b51017252e09ccdd715dec6c5e12a7d1ecfe"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,18 +12,18 @@
   <!--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="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <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"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--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="85e97290431ce6aa0a965421e84d6070cd899129"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <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="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- 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"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,18 +14,18 @@
   <!--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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="3326b51017252e09ccdd715dec6c5e12a7d1ecfe"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,18 +12,18 @@
   <!--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="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <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"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "ec3cab15f129926a6cfa5e95df71a0c913f34aee", 
+    "revision": "616fb21109ca9af6e0371a6d109dc4e3032ae4be", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,18 +12,18 @@
   <!--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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <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="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <!-- 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"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,18 +10,18 @@
   <!--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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <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="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <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"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,18 +12,18 @@
   <!--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="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
     <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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <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"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,18 +12,18 @@
   <!--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="85e97290431ce6aa0a965421e84d6070cd899129"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="bbb7659d8ea2afb396f99b3dc971ab3c42da3778"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b0ddb2122771e25cdd880359406a53b10cdc0507"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="230f11aff069d90d20fc2dc63b48e9ae3d4bdcd1"/>
   <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="96cdde4b5b5d8d3785b36c3c68cd746aff3005cc"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7f9ec13a30f1b2cc8bdb1a199b7da54b9ab8860f"/>
   <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"/>
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -414,16 +414,24 @@
 @BINPATH@/components/NetworkStatsManager.js
 @BINPATH@/components/NetworkStatsManager.manifest
 @BINPATH@/components/NetworkStatsServiceProxy.js
 @BINPATH@/components/NetworkStatsServiceProxy.manifest
 @BINPATH@/components/WifiWorker.js
 @BINPATH@/components/WifiWorker.manifest
 #endif // MOZ_WIDGET_GONK
 
+; ResourceStats
+#ifdef MOZ_WIDGET_GONK
+@BINPATH@/components/ResourceStats.js
+@BINPATH@/components/ResourceStats.manifest
+@BINPATH@/components/ResourceStatsManager.js
+@BINPATH@/components/ResourceStatsManager.manifest
+#endif // MOZ_WIDGET_GONK
+
 ; RIL
 #if defined(MOZ_WIDGET_GONK) && defined(MOZ_B2G_RIL)
 @BINPATH@/components/MmsService.js
 @BINPATH@/components/MmsService.manifest
 @BINPATH@/components/MobileMessageDatabaseService.js
 @BINPATH@/components/MobileMessageDatabaseService.manifest
 @BINPATH@/components/RadioInterfaceLayer.js
 @BINPATH@/components/RadioInterfaceLayer.manifest
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -777,17 +777,17 @@ let CustomizableUIInternal = {
 
       let container = areaNode.customizationTarget;
       let widgetNode = window.document.getElementById(aWidgetId);
       if (widgetNode && isOverflowable) {
         container = areaNode.overflowable.getContainerFor(widgetNode);
       }
 
       if (!widgetNode || !container.contains(widgetNode)) {
-        INFO("Widget not found, unable to remove");
+        INFO("Widget " + aWidgetId + " not found, unable to remove from " + aArea);
         continue;
       }
 
       this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
 
       // We remove location attributes here to make sure they're gone too when a
       // widget is removed from a toolbar to the palette. See bug 930950.
       this.removeLocationAttributes(widgetNode);
--- a/browser/devtools/canvasdebugger/test/browser.ini
+++ b/browser/devtools/canvasdebugger/test/browser.ini
@@ -1,25 +1,27 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   doc_simple-canvas.html
   doc_simple-canvas-bitmasks.html
   doc_simple-canvas-deep-stack.html
   doc_simple-canvas-transparent.html
+  doc_webgl-enum.html
   head.js
 
 [browser_canvas-actor-test-01.js]
 [browser_canvas-actor-test-02.js]
 [browser_canvas-actor-test-03.js]
 [browser_canvas-actor-test-04.js]
 [browser_canvas-actor-test-05.js]
 [browser_canvas-actor-test-06.js]
 [browser_canvas-actor-test-07.js]
 [browser_canvas-actor-test-08.js]
+[browser_canvas-actor-test-09.js]
 [browser_canvas-frontend-call-highlight.js]
 [browser_canvas-frontend-call-list.js]
 [browser_canvas-frontend-call-search.js]
 [browser_canvas-frontend-call-stack-01.js]
 [browser_canvas-frontend-call-stack-02.js]
 [browser_canvas-frontend-call-stack-03.js]
 [browser_canvas-frontend-clear.js]
 [browser_canvas-frontend-img-screenshots.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/browser_canvas-actor-test-09.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that integers used in arguments are not cast to their constant, enum value
+ * forms if the method's signature does not expect an enum. Bug 999687.
+ */
+
+function ifTestingSupported() {
+  let [target, debuggee, front] = yield initCanavsDebuggerBackend(WEBGL_ENUM_URL);
+
+  let navigated = once(target, "navigate");
+
+  yield front.setup({ reload: true });
+  ok(true, "The front was setup up successfully.");
+
+  yield navigated;
+  ok(true, "Target automatically navigated when the front was set up.");
+
+  let snapshotActor = yield front.recordAnimationFrame();
+  let animationOverview = yield snapshotActor.getOverview();
+  let functionCalls = animationOverview.calls;
+
+  is(functionCalls[0].name, "clear",
+    "The function's name is correct.");
+  is(functionCalls[0].argsPreview, "DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT | COLOR_BUFFER_BIT",
+    "The bits passed into `gl.clear` have been cast to their enum values.");
+
+  yield removeTab(target.tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/canvasdebugger/test/doc_webgl-enum.html
@@ -0,0 +1,33 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>WebGL editor test page</title>
+  </head>
+
+  <body>
+    <canvas id="canvas" width="128" height="128"></canvas>
+
+    <script type="text/javascript;version=1.8">
+      "use strict";
+
+      let canvas, gl;
+
+      window.onload = function() {
+        canvas = document.querySelector("canvas");
+        gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
+        gl.clearColor(0.0, 0.0, 0.0, 1.0);
+        drawScene();
+      }
+
+      function drawScene() {
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
+        window.requestAnimationFrame(drawScene);
+      }
+    </script>
+  </body>
+
+</html>
--- a/browser/devtools/canvasdebugger/test/head.js
+++ b/browser/devtools/canvasdebugger/test/head.js
@@ -24,16 +24,17 @@ let TiltGL = devtools.require("devtools/
 let TargetFactory = devtools.TargetFactory;
 let Toolbox = devtools.Toolbox;
 
 const EXAMPLE_URL = "http://example.com/browser/browser/devtools/canvasdebugger/test/";
 const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html";
 const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html";
 const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html";
 const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html";
+const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 let gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled");
 
 registerCleanupFunction(() => {
   info("finish() was called, cleaning up...");
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -393,16 +393,18 @@ HTMLBreadcrumbs.prototype = {
     this.container.removeEventListener("mousedown", this, true);
     this.container.removeEventListener("keypress", this, true);
     this.container = null;
 
     this.separators.remove();
     this.separators = null;
 
     this.nodeHierarchy = null;
+
+    this.isDestroyed = true;
   },
 
   /**
    * Empty the breadcrumbs container.
    */
   empty: function BC_empty()
   {
     while (this.container.hasChildNodes()) {
@@ -620,16 +622,20 @@ HTMLBreadcrumbs.prototype = {
     this.chromeWin.clearTimeout(this._ensureVisibleTimeout);
     this._ensureVisibleTimeout = this.chromeWin.setTimeout(function() {
       scrollbox.ensureElementIsVisible(element);
     }, ENSURE_SELECTION_VISIBLE_DELAY);
   },
 
   updateSelectors: function BC_updateSelectors()
   {
+    if (this.isDestroyed) {
+      return;
+    }
+
     for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
       let crumb = this.nodeHierarchy[i];
       let button = crumb.button;
 
       while(button.hasChildNodes()) {
         button.removeChild(button.firstChild);
       }
       button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
@@ -637,16 +643,20 @@ HTMLBreadcrumbs.prototype = {
     }
   },
 
   /**
    * Update the breadcrumbs display when a new node is selected.
    */
   update: function BC_update(reason)
   {
+    if (this.isDestroyed) {
+      return;
+    }
+
     if (reason !== "markupmutation") {
       this.inspector.hideNodeMenu();
     }
 
     let cmdDispatcher = this.chromeDoc.commandDispatcher;
     this.hadFocus = (cmdDispatcher.focusedElement &&
                      cmdDispatcher.focusedElement.parentNode == this.container);
 
@@ -683,16 +693,20 @@ HTMLBreadcrumbs.prototype = {
       // we select the current node button
       idx = this.indexOf(this.selection.nodeFront);
       this.setCursor(idx);
     }
 
     let doneUpdating = this.inspector.updating("breadcrumbs");
     // Add the first child of the very last node of the breadcrumbs if possible.
     this.ensureFirstChild().then(this.selectionGuard()).then(() => {
+      if (this.isDestroyed) {
+        return;
+      }
+
       this.updateSelectors();
 
       // Make sure the selected node and its neighbours are visible.
       this.scroll();
       return resolveNextTick().then(() => {
         this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
         doneUpdating();
       });
--- a/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoClass_menu.js
@@ -1,78 +1,69 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
+"use strict";
 
-  let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+// Test that the inspector has the correct pseudo-class locking menu items and
+// that these items actually work
 
-  let pseudos = ["hover", "active", "focus"];
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+const PSEUDOS = ["hover", "active", "focus"];
 
-  let doc;
-  let div;
-  let menu;
-  let inspector;
+let test = asyncTest(function*() {
+  yield addTab("data:text/html,pseudo-class lock node menu tests");
 
-  ignoreAllUncaughtExceptions();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    doc = content.document;
-    waitForFocus(createDocument, content);
-  }, true);
+  info("Creating the test element");
+  let div = content.document.createElement("div");
+  div.textContent = "test div";
+  content.document.body.appendChild(div);
 
-  content.location = "data:text/html,pseudo-class lock node menu tests";
+  let {inspector} = yield openInspector();
+  yield selectNode(div, inspector);
 
-  function createDocument()
-  {
-    div = doc.createElement("div");
-    div.textContent = "test div";
+  info("Getting the inspector ctx menu and opening it");
+  let menu = inspector.panelDoc.getElementById("inspector-node-popup");
+  yield openMenu(menu);
 
-    doc.body.appendChild(div);
+  yield testMenuItems(div, menu, inspector);
 
-    openInspector(selectNode);
-  }
+  gBrowser.removeCurrentTab();
+});
 
-  function selectNode(aInspector)
-  {
-    inspector = aInspector;
-    inspector.selection.setNode(div);
-    inspector.once("inspector-updated", performTests);
-  }
+function openMenu(menu) {
+  let def = promise.defer();
 
-  function performTests()
-  {
-    menu = inspector.panelDoc.getElementById("inspector-node-popup");
-    menu.addEventListener("popupshowing", testMenuItems, true);
-    menu.openPopup();
-  }
+  menu.addEventListener("popupshowing", function onOpen() {
+    menu.removeEventListener("popupshowing", onOpen, true);
+    def.resolve();
+  }, true);
+  menu.openPopup();
 
-  function testMenuItems()
-  {
-    menu.removeEventListener("popupshowing", testMenuItems, true);
+  return def.promise;
+}
 
-    var tryNext = () => {
-      if (pseudos.length === 0) {
-        finishUp();
-        return;
-      }
-      let pseudo = pseudos.shift();
-
-      let menuitem = inspector.panelDoc.getElementById("node-menu-pseudo-" + pseudo);
-      ok(menuitem, ":" + pseudo + " menuitem exists");
+function* testMenuItems(div,menu, inspector) {
+  for (let pseudo of PSEUDOS) {
+    let menuitem = inspector.panelDoc.getElementById("node-menu-pseudo-" + pseudo);
+    ok(menuitem, ":" + pseudo + " menuitem exists");
 
-      menuitem.doCommand();
-      inspector.selection.once("pseudoclass", () => {
-        is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
-          "pseudo-class lock has been applied");
-        tryNext();
-      });
-    }
-    tryNext();
-  }
+    // Give the inspector panels a chance to update when the pseudoclass changes
+    let onPseudo = inspector.selection.once("pseudoclass");
+    let onRefresh = inspector.once("rule-view-refreshed");
+    let onMutations = waitForMutation(inspector);
+
+    menuitem.doCommand();
 
-  function finishUp()
-  {
-    gBrowser.removeCurrentTab();
-    finish();
+    yield onPseudo;
+    yield onRefresh;
+    yield onMutations;
+
+    is(DOMUtils.hasPseudoClassLock(div, ":" + pseudo), true,
+      "pseudo-class lock has been applied");
   }
 }
+
+function waitForMutation(inspector) {
+  let def = promise.defer();
+  inspector.walker.once("mutations", def.resolve);
+  return def.promise;
+}
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -11,48 +11,41 @@ const TEST_URL = 'data:text/html,' +
                  '</head>' +
                  '<body>' +
                  '  <div id="parent-div">' +
                  '    <div id="div-1">test div</div>' +
                  '    <div id="div-2">test div2</div>' +
                  '  </div>' +
                  '</body>';
 
-function test() {
-  ignoreAllUncaughtExceptions();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(startTests, content);
-  }, true);
+let test = asyncTest(function*() {
+  info("Creating the test tab and opening the rule-view");
+  yield addTab(TEST_URL);
+  let {toolbox, inspector, view} = yield openRuleView();
 
-  content.location = TEST_URL;
-}
-
-let startTests = Task.async(function*() {
-  let {toolbox, inspector, view} = yield openRuleView();
+  info("Selecting the test node");
   yield selectNode("#div-1", inspector);
 
-  yield performTests(inspector, view);
-
-  yield finishUp(toolbox);
-  finish();
-});
-
-function* performTests(inspector, ruleview) {
   yield togglePseudoClass(inspector);
-  yield assertPseudoAddedToNode(inspector, ruleview);
+  yield assertPseudoAddedToNode(inspector, view);
 
   yield togglePseudoClass(inspector);
   yield assertPseudoRemovedFromNode();
-  yield assertPseudoRemovedFromView(inspector, ruleview);
+  yield assertPseudoRemovedFromView(inspector, view);
 
   yield togglePseudoClass(inspector);
-  yield testNavigate(inspector, ruleview);
-}
+  yield testNavigate(inspector, view);
+
+  info("Destroying the toolbox");
+  yield toolbox.destroy();
+  yield assertPseudoRemovedFromNode(getNode("#div-1"));
+
+  gBrowser.removeCurrentTab();
+});
+
 
 function* togglePseudoClass(inspector) {
   info("Toggle the pseudoclass, wait for it to be applied");
 
   // Give the inspector panels a chance to update when the pseudoclass changes
   let onPseudo = inspector.selection.once("pseudoclass");
   let onRefresh = inspector.once("rule-view-refreshed");
   let onMutations = waitForMutation(inspector);
@@ -134,17 +127,8 @@ function* assertPseudoRemovedFromView(in
   is(rules.length, 2, "rule view is showing 2 rules after removing lock");
 
   yield showPickerOn(getNode("#div-1"), inspector);
 
   let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
   is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
-
-function* finishUp(toolbox) {
-  let onDestroy = gDevTools.once("toolbox-destroyed");
-  toolbox.destroy();
-  yield onDestroy;
-
-  yield assertPseudoRemovedFromNode(getNode("#div-1"));
-  gBrowser.removeCurrentTab();
-}
--- a/browser/devtools/shared/Parser.jsm
+++ b/browser/devtools/shared/Parser.jsm
@@ -213,17 +213,17 @@ SyntaxTreesPool.prototype = {
           parseResults.scriptLength = syntaxTree.length;
           parseResults.scriptOffset = syntaxTree.offset;
           results.push(parseResults);
         }
       } catch (e) {
         // Can't guarantee that the tree traversal logic is forever perfect :)
         // Language features may be added, in which case the recursive methods
         // need to be updated. If an exception is thrown here, file a bug.
-        DevToolsUtils.reportException("Syntax tree visitor for " + aUrl, e);
+        DevToolsUtils.reportException("Syntax tree visitor for " + this._url, e);
       }
     }
     this._cache.set(requestId, results);
     return results;
   },
 
   _trees: null,
   _cache: null
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -190,21 +190,23 @@ function Tooltip(doc, options) {
     })(event);
     this.panel.addEventListener("popup" + event,
       this["_onPopup" + event], false);
   }
 
   // Listen to keypress events to close the tooltip if configured to do so
   let win = this.doc.querySelector("window");
   this._onKeyPress = event => {
+    if (this.panel.hidden) {
+      return;
+    }
+
     this.emit("keypress", event.keyCode);
     if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) {
-      if (!this.panel.hidden) {
-        event.stopPropagation();
-      }
+      event.stopPropagation();
       this.hide();
     }
   };
   win.addEventListener("keypress", this._onKeyPress, false);
 
   // Listen to custom emitters' events to close the tooltip
   this.hide = this.hide.bind(this);
   let closeOnEvents = this.options.get("closeOnEvents");
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -220,17 +220,18 @@ StyleSheetEditor.prototype = {
   },
 
   /**
    * Start fetching the full text source for this editor's sheet.
    */
   fetchSource: function(callback) {
     return this.styleSheet.getText().then((longStr) => {
       longStr.string().then((source) => {
-        this._state.text = CssLogic.prettifyCSS(source);
+        let ruleCount = this.styleSheet.ruleCount;
+        this._state.text = CssLogic.prettifyCSS(source, ruleCount);
         this.sourceLoaded = true;
 
         if (callback) {
           callback(source);
         }
         return source;
       });
     }, e => {
--- a/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_pretty.js
@@ -18,17 +18,17 @@ function test()
 
   content.location = TESTCASE_URI;
 }
 
 let editorTestedCount = 0;
 function testEditor(aEditor)
 {
   if (aEditor.styleSheet.styleSheetIndex == 0) {
-    let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n";
+    let prettifiedSource = "body\{\r?\n\tbackground\:white;\r?\n\}\r?\n\r?\ndiv\{\r?\n\tfont\-size\:4em;\r?\n\tcolor\:red\r?\n\}\r?\n\r?\nspan\{\r?\n\tcolor\:green;\r?\n\}\r?\n";
     let prettifiedSourceRE = new RegExp(prettifiedSource);
 
     ok(prettifiedSourceRE.test(aEditor.sourceEditor.getText()),
        "minified source has been prettified automatically");
     editorTestedCount++;
     let summary = gUI.editors[1].summary;
     EventUtils.synthesizeMouseAtCenter(summary, {}, gPanelWindow);
   }
--- a/browser/devtools/styleeditor/test/pretty.css
+++ b/browser/devtools/styleeditor/test/pretty.css
@@ -1,5 +1,2 @@
 
-
-body{background:white;}div{font-size:4em;color:red}
-
-
+body{background:white;}div{font-size:4em;color:red}span{color:green;}
--- a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js
@@ -1,59 +1,52 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Testing various inplace-editor behaviors in the rule-view
-// FIXME: To be split in several test files, and some of the inplace-editor
-// focus/blur/commit/revert stuff should be factored out in head.js
 
 let TEST_URL = 'url("' + TEST_URL_ROOT + 'doc_test_image.png")';
 let PAGE_CONTENT = [
   '<style type="text/css">',
   '  #testid {',
   '    background-color: blue;',
   '  }',
-  '  .testclass {',
-  '    background-color: green;',
-  '  }',
   '</style>',
-  '<div id="testid" class="testclass" style="background-color:red;">Styled Node</div>'
+  '<div id="testid">Styled Node</div>'
 ].join("\n");
 
 let test = asyncTest(function*() {
   yield addTab("data:text/html,test rule view user changes");
 
   info("Creating the test document");
   content.document.body.innerHTML = PAGE_CONTENT;
 
   info("Opening the rule-view");
   let {toolbox, inspector, view} = yield openRuleView();
 
   info("Selecting the test element");
   yield selectNode("#testid", inspector);
 
-  yield testCreateNewEscape(view);
-});
-
-function* testCreateNewEscape(view) {
   info("Test creating a new property and escaping");
 
-  let elementRuleEditor = getRuleViewRuleEditor(view, 0);
+  let elementRuleEditor = getRuleViewRuleEditor(view, 1);
 
   info("Focusing a new property name in the rule-view");
   let editor = yield focusEditableField(elementRuleEditor.closeBrace);
 
   is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "The new property editor got focused.");
   let input = editor.input;
 
   info("Entering a value in the property name editor");
+  let onModifications = elementRuleEditor.rule._applyingModifications;
   input.value = "color";
+  yield onModifications;
 
   info("Pressing return to commit and focus the new value field");
   let onValueFocus = once(elementRuleEditor.element, "focus", true);
   let onModifications = elementRuleEditor.rule._applyingModifications;
   EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
   yield onValueFocus;
   yield onModifications;
 
@@ -64,21 +57,23 @@ function* testCreateNewEscape(view) {
   is(elementRuleEditor.rule.textProps.length,  2, "Created a new text property.");
   is(elementRuleEditor.propertyList.children.length, 2, "Created a property editor.");
   is(editor, inplaceEditor(textProp.editor.valueSpan), "Editing the value span now.");
 
   info("Entering a property value");
   editor.input.value = "red";
 
   info("Escaping out of the field");
+  let onModifications = elementRuleEditor.rule._applyingModifications;
   EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
+  yield onModifications;
 
   info("Checking that the previous field is focused");
   let focusedElement = inplaceEditor(elementRuleEditor.rule.textProps[0].editor.valueSpan).input;
   is(focusedElement, focusedElement.ownerDocument.activeElement, "Correct element has focus");
 
   let onModifications = elementRuleEditor.rule._applyingModifications;
   EventUtils.synthesizeKey("VK_ESCAPE", {}, view.doc.defaultView);
   yield onModifications;
 
   is(elementRuleEditor.rule.textProps.length,  1, "Removed the new text property.");
   is(elementRuleEditor.propertyList.children.length, 1, "Removed the property editor.");
-}
+});
--- a/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_graph-render-01.js
@@ -1,33 +1,46 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that SVG nodes and edges were created for the Graph View.
  */
 
+let connectCount = 0;
+
 function spawnTest() {
   let [target, debuggee, panel] = yield initWebAudioEditor(SIMPLE_CONTEXT_URL);
   let { panelWin } = panel;
   let { gFront, $, $$, EVENTS } = panelWin;
 
   let started = once(gFront, "start-context");
 
   reload(target);
 
+  panelWin.on(EVENTS.CONNECT_NODE, onConnectNode);
+
   let [actors] = yield Promise.all([
     get3(gFront, "create-node"),
     waitForGraphRendered(panelWin, 3, 2)
   ]);
 
   let [destId, oscId, gainId] = actors.map(actor => actor.actorID);
 
   ok(findGraphNode(panelWin, oscId).classList.contains("type-OscillatorNode"), "found OscillatorNode with class");
   ok(findGraphNode(panelWin, gainId).classList.contains("type-GainNode"), "found GainNode with class");
   ok(findGraphNode(panelWin, destId).classList.contains("type-AudioDestinationNode"), "found AudioDestinationNode with class");
   is(findGraphEdge(panelWin, oscId, gainId).toString(), "[object SVGGElement]", "found edge for osc -> gain");
   is(findGraphEdge(panelWin, gainId, destId).toString(), "[object SVGGElement]", "found edge for gain -> dest");
 
+  yield wait(1000);
+
+  is(connectCount, 2, "Only two node connect events should be fired.");
+
+  panelWin.off(EVENTS.CONNECT_NODE, onConnectNode);
+
   yield teardown(panel);
   finish();
 }
 
+function onConnectNode () {
+  ++connectCount;
+}
--- a/browser/devtools/webaudioeditor/test/doc_simple-context.html
+++ b/browser/devtools/webaudioeditor/test/doc_simple-context.html
@@ -12,15 +12,22 @@
 
     <script type="text/javascript;version=1.8">
       "use strict";
 
       let ctx = new AudioContext();
       let osc = ctx.createOscillator();
       let gain = ctx.createGain();
       gain.gain.value = 0;
+
+      // Connect multiple times to test that it's disregarded.
       osc.connect(gain);
       gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
+      gain.connect(ctx.destination);
       osc.start(0);
     </script>
   </body>
 
 </html>
--- a/browser/devtools/webaudioeditor/webaudioeditor-controller.js
+++ b/browser/devtools/webaudioeditor/webaudioeditor-controller.js
@@ -89,29 +89,34 @@ function AudioNodeView (actor) {
 // A proxy for the underlying AudioNodeActor to fetch its type
 // and subsequently assign the type to the instance.
 AudioNodeView.prototype.getType = Task.async(function* () {
   this.type = yield this.actor.getType();
   return this.type;
 });
 
 // Helper method to create connections in the AudioNodeConnections
-// WeakMap for rendering
+// WeakMap for rendering. Returns a boolean indicating
+// if the connection was successfully created. Will return `false`
+// when the connection was previously made.
 AudioNodeView.prototype.connect = function (destination) {
-  let connections = AudioNodeConnections.get(this);
-  if (!connections) {
-    connections = [];
-    AudioNodeConnections.set(this, connections);
+  let connections = AudioNodeConnections.get(this) || new Set();
+  AudioNodeConnections.set(this, connections);
+
+  // Don't duplicate add.
+  if (!connections.has(destination)) {
+    connections.add(destination);
+    return true;
   }
-  connections.push(destination);
+  return false;
 };
 
 // Helper method to remove audio connections from the current AudioNodeView
 AudioNodeView.prototype.disconnect = function () {
-  AudioNodeConnections.set(this, []);
+  AudioNodeConnections.set(this, new Set());
 };
 
 // Returns a promise that resolves to an array of objects containing
 // both a `param` name property and a `value` property.
 AudioNodeView.prototype.getParams = function () {
   return this.actor.getParams();
 };
 
@@ -288,18 +293,20 @@ let WebAudioEditorController = {
   _onConnectNode: Task.async(function* ({ source: sourceActor, dest: destActor }) {
     // Since node create and connect are probably executed back to back,
     // and the controller's `_onCreateNode` needs to look up type,
     // the edge creation could be called before the graph node is actually
     // created. This way, we can check and listen for the event before
     // adding an edge.
     let [source, dest] = yield waitForNodeCreation(sourceActor, destActor);
 
-    source.connect(dest);
-    window.emit(EVENTS.CONNECT_NODE, source.id, dest.id);
+    // Connect nodes, and only emit if it's a new connection.
+    if (source.connect(dest)) {
+      window.emit(EVENTS.CONNECT_NODE, source.id, dest.id);
+    }
 
     function waitForNodeCreation (sourceActor, destActor) {
       let deferred = defer();
       let source = getViewNodeByActor(sourceActor);
       let dest = getViewNodeByActor(destActor);
 
       if (!source || !dest)
         window.on(EVENTS.CREATE_NODE, function createNodeListener (_, id) {
--- a/browser/devtools/webaudioeditor/webaudioeditor-view.js
+++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js
@@ -156,17 +156,17 @@ let WebAudioGraphView = {
 
     AudioNodes.forEach(node => {
       // Add node to graph
       graph.addNode(node.id, { label: node.type, id: node.id });
 
       // Add all of the connections from this node to the edge array to be added
       // after all the nodes are added, otherwise edges will attempted to be created
       // for nodes that have not yet been added
-      AudioNodeConnections.get(node, []).forEach(dest => edges.push([node, dest]));
+      AudioNodeConnections.get(node, new Set()).forEach(dest => edges.push([node, dest]));
     });
 
     edges.forEach(([node, dest]) => graph.addEdge(null, node.id, dest.id, {
       source: node.id,
       target: dest.id
     }));
 
     let renderer = new dagreD3.Renderer();
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -181,16 +181,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "networkstats-manage": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
+                           "resourcestats-manage": {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
                            "wifi-manage": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "systemXHR": {
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -164,16 +164,20 @@ DOMInterfaces = {
 'BluetoothDevice': {
     'nativeType': 'mozilla::dom::bluetooth::BluetoothDevice',
 },
 
 'BluetoothManager': {
     'nativeType': 'mozilla::dom::bluetooth::BluetoothManager',
 },
 
+'BluetoothDiscoveryHandle': {
+    'nativeType': 'mozilla::dom::bluetooth::BluetoothDiscoveryHandle',
+},
+
 'CameraCapabilities': {
     'nativeType': 'mozilla::dom::CameraCapabilities',
     'headerFile': 'DOMCameraCapabilities.h'
 },
 
 'CameraControl': {
     'nativeType': 'mozilla::nsDOMCameraControl',
     'headerFile': 'DOMCameraControl.h',
--- a/dom/bluetooth2/BluetoothAdapter.cpp
+++ b/dom/bluetooth2/BluetoothAdapter.cpp
@@ -16,16 +16,17 @@
 #include "mozilla/dom/BluetoothDeviceEvent.h"
 #include "mozilla/dom/BluetoothStatusChangedEvent.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/LazyIdleThread.h"
 
 #include "BluetoothAdapter.h"
 #include "BluetoothDevice.h"
+#include "BluetoothDiscoveryHandle.h"
 #include "BluetoothReplyRunnable.h"
 #include "BluetoothService.h"
 #include "BluetoothUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 USING_BLUETOOTH_NAMESPACE
@@ -50,16 +51,59 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 // QueryInterface implementation for BluetoothAdapter
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(BluetoothAdapter)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(BluetoothAdapter, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(BluetoothAdapter, DOMEventTargetHelper)
 
+class StartDiscoveryTask : public BluetoothReplyRunnable
+{
+ public:
+  StartDiscoveryTask(BluetoothAdapter* aAdapter, Promise* aPromise)
+    : BluetoothReplyRunnable(nullptr, aPromise,
+                             NS_LITERAL_STRING("StartDiscovery"))
+    , mAdapter(aAdapter)
+  {
+    MOZ_ASSERT(aPromise && aAdapter);
+  }
+
+  bool
+  ParseSuccessfulReply(JS::MutableHandle<JS::Value> aValue)
+  {
+    BT_API2_LOGR();
+    aValue.setUndefined();
+
+    AutoJSAPI jsapi;
+    NS_ENSURE_TRUE(jsapi.Init(mAdapter->GetParentObject()), false);
+
+    // Wrap BluetoothDiscoveryHandle to return
+    JSContext* cx = jsapi.cx();
+    nsRefPtr<BluetoothDiscoveryHandle> discoveryHandle =
+      BluetoothDiscoveryHandle::Create(mAdapter->GetParentObject());
+    if (!ToJSValue(cx, discoveryHandle, aValue)) {
+      JS_ClearPendingException(cx);
+      return false;
+    }
+
+    return true;
+  }
+
+  virtual void
+  ReleaseMembers() MOZ_OVERRIDE
+  {
+    BluetoothReplyRunnable::ReleaseMembers();
+    mAdapter = nullptr;
+  }
+
+private:
+  nsRefPtr<BluetoothAdapter> mAdapter;
+};
+
 class GetDevicesTask : public BluetoothReplyRunnable
 {
 public:
   GetDevicesTask(BluetoothAdapter* aAdapterPtr,
                        nsIDOMDOMRequest* aReq) :
     BluetoothReplyRunnable(aReq),
     mAdapterPtr(aAdapterPtr)
   {
@@ -299,34 +343,20 @@ BluetoothAdapter::Create(nsPIDOMWindow* 
   return adapter.forget();
 }
 
 void
 BluetoothAdapter::Notify(const BluetoothSignal& aData)
 {
   InfallibleTArray<BluetoothNamedValue> arr;
 
-  BT_LOGD("[A] %s: %s", __FUNCTION__,
-          NS_ConvertUTF16toUTF8(aData.name()).get());
+  BT_LOGD("[A] %s", NS_ConvertUTF16toUTF8(aData.name()).get());
 
   BluetoothValue v = aData.value();
-  if (aData.name().EqualsLiteral("DeviceFound")) {
-    nsRefPtr<BluetoothDevice> device =
-      BluetoothDevice::Create(GetOwner(), aData.value());
-
-    BluetoothDeviceEventInit init;
-    init.mBubbles = false;
-    init.mCancelable = false;
-    init.mDevice = device;
-    nsRefPtr<BluetoothDeviceEvent> event =
-      BluetoothDeviceEvent::Constructor(this,
-                                        NS_LITERAL_STRING("devicefound"),
-                                        init);
-    DispatchTrustedEvent(event);
-  } else if (aData.name().EqualsLiteral("PropertyChanged")) {
+  if (aData.name().EqualsLiteral("PropertyChanged")) {
     HandlePropertyChanged(v);
   } else if (aData.name().EqualsLiteral(PAIRED_STATUS_CHANGED_ID) ||
              aData.name().EqualsLiteral(HFP_STATUS_CHANGED_ID) ||
              aData.name().EqualsLiteral(SCO_STATUS_CHANGED_ID) ||
              aData.name().EqualsLiteral(A2DP_STATUS_CHANGED_ID)) {
     MOZ_ASSERT(v.type() == BluetoothValue::TArrayOfBluetoothNamedValue);
     const InfallibleTArray<BluetoothNamedValue>& arr =
       v.get_ArrayOfBluetoothNamedValue();
@@ -355,59 +385,65 @@ BluetoothAdapter::Notify(const Bluetooth
 
     DispatchTrustedEvent(event);
   } else {
     BT_WARNING("Not handling adapter signal: %s",
                NS_ConvertUTF16toUTF8(aData.name()).get());
   }
 }
 
-already_AddRefed<DOMRequest>
+already_AddRefed<Promise>
 BluetoothAdapter::StartStopDiscovery(bool aStart, ErrorResult& aRv)
 {
-  nsCOMPtr<nsPIDOMWindow> win = GetOwner();
-  if (!win) {
-    aRv.Throw(NS_ERROR_FAILURE);
-    return nullptr;
-  }
-
-  nsRefPtr<DOMRequest> request = new DOMRequest(win);
-  nsRefPtr<BluetoothVoidReplyRunnable> results =
-    new BluetoothVoidReplyRunnable(request);
-
-  BluetoothService* bs = BluetoothService::Get();
-  if (!bs) {
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
+  if (!global) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
+  nsRefPtr<Promise> promise = new Promise(global);
+
+  /**
+   * Ensure
+   * - adapter does not already start/stop discovering,
+   * - adapter is already enabled, and
+   * - BluetoothService is available
+   */
+  BT_ENSURE_TRUE_RESOLVE(mDiscovering != aStart, JS::UndefinedHandleValue);
+  BT_ENSURE_TRUE_REJECT(mState == BluetoothAdapterState::Enabled,
+                        NS_ERROR_DOM_INVALID_STATE_ERR);
+  BluetoothService* bs = BluetoothService::Get();
+  BT_ENSURE_TRUE_REJECT(bs, NS_ERROR_NOT_AVAILABLE);
+
+  BT_API2_LOGR("aStart %d", aStart);
   nsresult rv;
   if (aStart) {
-    rv = bs->StartDiscoveryInternal(results);
+    // Start discovery: return BluetoothDiscoveryHandle in StartDiscoveryTask
+    nsRefPtr<BluetoothReplyRunnable> result =
+      new StartDiscoveryTask(this, promise);
+    rv = bs->StartDiscoveryInternal(result);
   } else {
-    rv = bs->StopDiscoveryInternal(results);
+    // Stop discovery: void return
+    nsRefPtr<BluetoothReplyRunnable> result =
+      new BluetoothVoidReplyRunnable(nullptr /* DOMRequest */,
+                                     promise,
+                                     NS_LITERAL_STRING("StopDiscovery"));
+    rv = bs->StopDiscoveryInternal(result);
   }
-  if (NS_FAILED(rv)) {
-    BT_WARNING("Start/Stop Discovery failed!");
-    aRv.Throw(rv);
-    return nullptr;
-  }
+  BT_ENSURE_TRUE_REJECT(NS_SUCCEEDED(rv), NS_ERROR_DOM_OPERATION_ERR);
 
-  // mDiscovering is not set here, we'll get a Property update from our external
-  // protocol to tell us that it's been set.
-
-  return request.forget();
+  return promise.forget();
 }
 
-already_AddRefed<DOMRequest>
+already_AddRefed<Promise>
 BluetoothAdapter::StartDiscovery(ErrorResult& aRv)
 {
   return StartStopDiscovery(true, aRv);
 }
 
-already_AddRefed<DOMRequest>
+already_AddRefed<Promise>
 BluetoothAdapter::StopDiscovery(ErrorResult& aRv)
 {
   return StartStopDiscovery(false, aRv);
 }
 
 void
 BluetoothAdapter::GetDevices(JSContext* aContext,
                              JS::MutableHandle<JS::Value> aDevices,
@@ -441,81 +477,80 @@ BluetoothAdapter::GetUuids(JSContext* aC
 already_AddRefed<Promise>
 BluetoothAdapter::SetName(const nsAString& aName, ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
   if(!global) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
-
   nsRefPtr<Promise> promise = new Promise(global);
 
-  if (mName.Equals(aName)) {
-    // Need to resolved with "undefined" since this method is Promise<void>
-    promise->MaybeResolve(JS::UndefinedHandleValue);
-    return promise.forget();
-  }
+  /**
+   * Ensure
+   * - adapter's name does not equal to aName,
+   * - adapter is already enabled, and
+   * - BluetoothService is available
+   */
+  BT_ENSURE_TRUE_RESOLVE(!mName.Equals(aName), JS::UndefinedHandleValue);
+  BT_ENSURE_TRUE_REJECT(mState == BluetoothAdapterState::Enabled,
+                        NS_ERROR_DOM_INVALID_STATE_ERR);
+  BluetoothService* bs = BluetoothService::Get();
+  BT_ENSURE_TRUE_REJECT(bs, NS_ERROR_NOT_AVAILABLE);
 
+  // Wrap property to set and runnable to handle result
   nsString name(aName);
-  BluetoothValue value(name);
-  BluetoothNamedValue property(NS_LITERAL_STRING("Name"), value);
-
-  BluetoothService* bs = BluetoothService::Get();
-  if (!bs) {
-    promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
-    return promise.forget();
-  }
-
+  BluetoothNamedValue property(NS_LITERAL_STRING("Name"),
+                               BluetoothValue(name));
   nsRefPtr<BluetoothReplyRunnable> result =
     new BluetoothVoidReplyRunnable(nullptr /* DOMRequest */,
                                    promise,
                                    NS_LITERAL_STRING("SetName"));
-  if (NS_FAILED(bs->SetProperty(BluetoothObjectType::TYPE_ADAPTER,
-                               property, result))) {
-    promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
-  }
+  BT_ENSURE_TRUE_REJECT(
+    NS_SUCCEEDED(bs->SetProperty(BluetoothObjectType::TYPE_ADAPTER,
+                                 property, result)),
+    NS_ERROR_DOM_OPERATION_ERR);
 
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 BluetoothAdapter::SetDiscoverable(bool aDiscoverable, ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
   if(!global) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
-
   nsRefPtr<Promise> promise = new Promise(global);
 
-  if (aDiscoverable == mDiscoverable) {
-    // Need to resolved with "undefined" since this method is Promise<void>
-    promise->MaybeResolve(JS::UndefinedHandleValue);
-    return promise.forget();
-  }
+  /**
+   * Ensure
+   * - mDiscoverable does not equal to aDiscoverable,
+   * - adapter is already enabled, and
+   * - BluetoothService is available
+   */
+  BT_ENSURE_TRUE_RESOLVE(mDiscoverable != aDiscoverable,
+                         JS::UndefinedHandleValue);
+  BT_ENSURE_TRUE_REJECT(mState == BluetoothAdapterState::Enabled,
+                        NS_ERROR_DOM_INVALID_STATE_ERR);
+  BluetoothService* bs = BluetoothService::Get();
+  BT_ENSURE_TRUE_REJECT(bs, NS_ERROR_NOT_AVAILABLE);
 
-  BluetoothValue value(aDiscoverable);
-  BluetoothNamedValue property(NS_LITERAL_STRING("Discoverable"), value);
-
-  BluetoothService* bs = BluetoothService::Get();
-  if (!bs) {
-    promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
-    return promise.forget();
-  }
-
+  // Wrap property to set and runnable to handle result
+  BluetoothNamedValue property(NS_LITERAL_STRING("Discoverable"),
+                               BluetoothValue(aDiscoverable));
   nsRefPtr<BluetoothReplyRunnable> result =
     new BluetoothVoidReplyRunnable(nullptr /* DOMRequest */,
                                    promise,
                                    NS_LITERAL_STRING("SetDiscoverable"));
-  if (NS_FAILED(bs->SetProperty(BluetoothObjectType::TYPE_ADAPTER,
-                                property, result))) {
-    promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
-  }
+  BT_ENSURE_TRUE_REJECT(
+    NS_SUCCEEDED(bs->SetProperty(BluetoothObjectType::TYPE_ADAPTER,
+                                 property, result)),
+    NS_ERROR_DOM_OPERATION_ERR);
 
   return promise.forget();
 }
 
 already_AddRefed<DOMRequest>
 BluetoothAdapter::GetConnectedDevices(uint16_t aServiceUuid, ErrorResult& aRv)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -708,58 +743,54 @@ BluetoothAdapter::SetPairingConfirmation
 already_AddRefed<Promise>
 BluetoothAdapter::EnableDisable(bool aEnable, ErrorResult& aRv)
 {
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
   if(!global) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
-
   nsRefPtr<Promise> promise = new Promise(global);
 
   // Ensure BluetoothService is available before modifying adapter state
   BluetoothService* bs = BluetoothService::Get();
-  if (!bs) {
-    promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
-    return promise.forget();
-  }
+  BT_ENSURE_TRUE_REJECT(bs, NS_ERROR_NOT_AVAILABLE);
 
-  nsString methodName;
+  // Modify adapter state to Enabling/Disabling if adapter is in a valid state
+  nsAutoString methodName;
   if (aEnable) {
     // Enable local adapter
-    if (mState != BluetoothAdapterState::Disabled) {
-      promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
-      return promise.forget();
-    }
+    BT_ENSURE_TRUE_REJECT(mState == BluetoothAdapterState::Disabled,
+                          NS_ERROR_DOM_INVALID_STATE_ERR);
     methodName.AssignLiteral("Enable");
     mState = BluetoothAdapterState::Enabling;
   } else {
     // Disable local adapter
-    if (mState != BluetoothAdapterState::Enabled) {
-      promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
-      return promise.forget();
-    }
+    BT_ENSURE_TRUE_REJECT(mState == BluetoothAdapterState::Enabled,
+                          NS_ERROR_DOM_INVALID_STATE_ERR);
     methodName.AssignLiteral("Disable");
     mState = BluetoothAdapterState::Disabling;
   }
 
+  // Notify applications of adapter state change to Enabling/Disabling
   nsTArray<nsString> types;
   BT_APPEND_ENUM_STRING(types,
                         BluetoothAdapterAttribute,
                         BluetoothAdapterAttribute::State);
-
   DispatchAttributeEvent(types);
 
+  // Wrap runnable to handle result
   nsRefPtr<BluetoothReplyRunnable> result =
     new BluetoothVoidReplyRunnable(nullptr, /* DOMRequest */
                                    promise,
                                    methodName);
 
   if(NS_FAILED(bs->EnableDisable(aEnable, result))) {
+    mState = aEnable ? BluetoothAdapterState::Disabled
+                     : BluetoothAdapterState::Enabled;
     promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
   }
 
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 BluetoothAdapter::Enable(ErrorResult& aRv)
--- a/dom/bluetooth2/BluetoothAdapter.h
+++ b/dom/bluetooth2/BluetoothAdapter.h
@@ -91,18 +91,18 @@ public:
   void GetDevices(JSContext* aContext, JS::MutableHandle<JS::Value> aDevices,
                   ErrorResult& aRv);
   void GetUuids(JSContext* aContext, JS::MutableHandle<JS::Value> aUuids,
                 ErrorResult& aRv);
 
   already_AddRefed<Promise> SetName(const nsAString& aName, ErrorResult& aRv);
   already_AddRefed<Promise>
     SetDiscoverable(bool aDiscoverable, ErrorResult& aRv);
-  already_AddRefed<DOMRequest> StartDiscovery(ErrorResult& aRv);
-  already_AddRefed<DOMRequest> StopDiscovery(ErrorResult& aRv);
+  already_AddRefed<Promise> StartDiscovery(ErrorResult& aRv);
+  already_AddRefed<Promise> StopDiscovery(ErrorResult& aRv);
 
   already_AddRefed<DOMRequest>
     Pair(const nsAString& aDeviceAddress, ErrorResult& aRv);
   already_AddRefed<DOMRequest>
     Unpair(const nsAString& aDeviceAddress, ErrorResult& aRv);
   already_AddRefed<DOMRequest>
     GetPairedDevices(ErrorResult& aRv);
   already_AddRefed<DOMRequest>
@@ -171,18 +171,17 @@ public:
     WrapObject(JSContext* aCx) MOZ_OVERRIDE;
 
 private:
   BluetoothAdapter(nsPIDOMWindow* aOwner, const BluetoothValue& aValue);
   ~BluetoothAdapter();
 
   void Root();
 
-  already_AddRefed<mozilla::dom::DOMRequest>
-    StartStopDiscovery(bool aStart, ErrorResult& aRv);
+  already_AddRefed<Promise> StartStopDiscovery(bool aStart, ErrorResult& aRv);
   already_AddRefed<mozilla::dom::DOMRequest>
     PairUnpair(bool aPair, const nsAString& aDeviceAddress, ErrorResult& aRv);
 
   bool IsAdapterAttributeChanged(BluetoothAdapterAttribute aType,
                                  const BluetoothValue& aValue);
   void HandlePropertyChanged(const BluetoothValue& aValue);
   void DispatchAttributeEvent(const nsTArray<nsString>& aTypes);
   BluetoothAdapterAttribute
--- a/dom/bluetooth2/BluetoothCommon.h
+++ b/dom/bluetooth2/BluetoothCommon.h
@@ -90,27 +90,52 @@ extern bool gBluetoothDebugFlag;
   do {                                                               \
     uint32_t index = uint32_t(enumValue);                            \
     nsAutoString name;                                               \
     name.AssignASCII(enumType##Values::strings[index].value,         \
                      enumType##Values::strings[index].length);       \
     array.AppendElement(name);                                       \
   } while(0)                                                         \
 
+/**
+ * Resolve promise with |ret| if |x| is false.
+ */
+#define BT_ENSURE_TRUE_RESOLVE(x, ret)                               \
+  do {                                                               \
+    if (MOZ_UNLIKELY(!(x))) {                                        \
+      BT_API2_LOGR("BT_ENSURE_TRUE_RESOLVE(" #x ") failed");         \
+      promise->MaybeResolve(ret);                                    \
+      return promise.forget();                                       \
+    }                                                                \
+  } while(0)
+
+/**
+ * Reject promise with |ret| if |x| is false.
+ */
+#define BT_ENSURE_TRUE_REJECT(x, ret)                                \
+  do {                                                               \
+    if (MOZ_UNLIKELY(!(x))) {                                        \
+      BT_API2_LOGR("BT_ENSURE_TRUE_REJECT(" #x ") failed");          \
+      promise->MaybeReject(ret);                                     \
+      return promise.forget();                                       \
+    }                                                                \
+  } while(0)
+
 #define BEGIN_BLUETOOTH_NAMESPACE \
   namespace mozilla { namespace dom { namespace bluetooth {
 #define END_BLUETOOTH_NAMESPACE \
   } /* namespace bluetooth */ } /* namespace dom */ } /* namespace mozilla */
 #define USING_BLUETOOTH_NAMESPACE \
   using namespace mozilla::dom::bluetooth;
 
-#define KEY_LOCAL_AGENT  "/B2G/bluetooth/agent"
-#define KEY_REMOTE_AGENT "/B2G/bluetooth/remote_device_agent"
-#define KEY_MANAGER      "/B2G/bluetooth/manager"
-#define KEY_ADAPTER      "/B2G/bluetooth/adapter"
+#define KEY_LOCAL_AGENT       "/B2G/bluetooth/agent"
+#define KEY_REMOTE_AGENT      "/B2G/bluetooth/remote_device_agent"
+#define KEY_MANAGER           "/B2G/bluetooth/manager"
+#define KEY_ADAPTER           "/B2G/bluetooth/adapter"
+#define KEY_DISCOVERY_HANDLE  "/B2G/bluetooth/discovery_handle"
 
 /**
  * When the connection status of a Bluetooth profile is changed, we'll notify
  * observers which register the following topics.
  */
 #define BLUETOOTH_A2DP_STATUS_CHANGED_ID "bluetooth-a2dp-status-changed"
 #define BLUETOOTH_HFP_STATUS_CHANGED_ID  "bluetooth-hfp-status-changed"
 #define BLUETOOTH_HID_STATUS_CHANGED_ID  "bluetooth-hid-status-changed"
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth2/BluetoothDiscoveryHandle.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BluetoothDiscoveryHandle.h"
+#include "BluetoothService.h"
+
+#include "mozilla/dom/bluetooth/BluetoothTypes.h"
+#include "mozilla/dom/BluetoothDeviceEvent.h"
+#include "mozilla/dom/BluetoothDiscoveryHandleBinding.h"
+#include "nsThreadUtils.h"
+
+USING_BLUETOOTH_NAMESPACE
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(BluetoothDiscoveryHandle)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(BluetoothDiscoveryHandle, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(BluetoothDiscoveryHandle, DOMEventTargetHelper)
+
+BluetoothDiscoveryHandle::BluetoothDiscoveryHandle(nsPIDOMWindow* aWindow)
+  : DOMEventTargetHelper(aWindow)
+{
+  MOZ_ASSERT(aWindow);
+  MOZ_ASSERT(IsDOMBinding());
+
+  ListenToBluetoothSignal(true);
+}
+
+BluetoothDiscoveryHandle::~BluetoothDiscoveryHandle()
+{
+  ListenToBluetoothSignal(false);
+}
+
+void
+BluetoothDiscoveryHandle::ListenToBluetoothSignal(bool aStart)
+{
+  BluetoothService* bs = BluetoothService::Get();
+  NS_ENSURE_TRUE_VOID(bs);
+
+  if (aStart) {
+    bs->RegisterBluetoothSignalHandler(
+      NS_LITERAL_STRING(KEY_DISCOVERY_HANDLE), this);
+  } else {
+    bs->UnregisterBluetoothSignalHandler(
+      NS_LITERAL_STRING(KEY_DISCOVERY_HANDLE), this);
+  }
+}
+
+// static
+already_AddRefed<BluetoothDiscoveryHandle>
+BluetoothDiscoveryHandle::Create(nsPIDOMWindow* aWindow)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aWindow);
+
+  nsRefPtr<BluetoothDiscoveryHandle> handle =
+    new BluetoothDiscoveryHandle(aWindow);
+  return handle.forget();
+}
+
+void
+BluetoothDiscoveryHandle::DispatchDeviceEvent(const BluetoothValue& aValue)
+{
+  // Create a new BluetoothDevice
+  nsRefPtr<BluetoothDevice> device =
+    BluetoothDevice::Create(GetOwner(), aValue);
+
+  // Notify application of discovered device
+  BluetoothDeviceEventInit init;
+  init.mDevice = device;
+  nsRefPtr<BluetoothDeviceEvent> event =
+    BluetoothDeviceEvent::Constructor(this,
+                                      NS_LITERAL_STRING("devicefound"),
+                                      init);
+  DispatchTrustedEvent(event);
+}
+
+void
+BluetoothDiscoveryHandle::Notify(const BluetoothSignal& aData)
+{
+  BT_LOGD("[DH] %s", NS_ConvertUTF16toUTF8(aData.name()).get());
+
+  if (aData.name().EqualsLiteral("DeviceFound")) {
+    DispatchDeviceEvent(aData.value());
+  } else {
+    BT_WARNING("Not handling discovery handle signal: %s",
+               NS_ConvertUTF16toUTF8(aData.name()).get());
+  }
+}
+
+JSObject*
+BluetoothDiscoveryHandle::WrapObject(JSContext* aCx)
+{
+  return BluetoothDiscoveryHandleBinding::Wrap(aCx, this);
+}
new file mode 100644
--- /dev/null
+++ b/dom/bluetooth2/BluetoothDiscoveryHandle.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_bluetooth_bluetoothdiscoveryhandle_h
+#define mozilla_dom_bluetooth_bluetoothdiscoveryhandle_h
+
+#include "BluetoothCommon.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/bluetooth/BluetoothTypes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/Observer.h"
+#include "nsISupportsImpl.h"
+
+BEGIN_BLUETOOTH_NAMESPACE
+
+class BluetoothValue;
+
+class BluetoothDiscoveryHandle MOZ_FINAL : public DOMEventTargetHelper
+                                         , public BluetoothSignalObserver
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+
+  static already_AddRefed<BluetoothDiscoveryHandle>
+    Create(nsPIDOMWindow* aWindow);
+
+  IMPL_EVENT_HANDLER(devicefound);
+
+  void Notify(const BluetoothSignal& aData); // BluetoothSignalObserver
+  virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
+
+private:
+  BluetoothDiscoveryHandle(nsPIDOMWindow* aWindow);
+  ~BluetoothDiscoveryHandle();
+
+  /**
+   * Start/Stop listening to bluetooth signal.
+   *
+   * @param aStart [in] Whether to start or stop listening to bluetooth signal
+   */
+  void ListenToBluetoothSignal(bool aStart);
+
+  /**
+   * Fire BluetoothDeviceEvent to trigger ondevicefound event handler.
+   *
+   * @param aValue [in] Properties array of the found device
+   */
+  void DispatchDeviceEvent(const BluetoothValue& aValue);
+};
+
+END_BLUETOOTH_NAMESPACE
+
+#endif // mozilla_dom_bluetooth_bluetoothdiscoveryhandle_h
--- a/dom/bluetooth2/BluetoothManager.cpp
+++ b/dom/bluetooth2/BluetoothManager.cpp
@@ -1,17 +1,16 @@
 /* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "base/basictypes.h"
 #include "BluetoothManager.h"
-#include "BluetoothCommon.h"
 #include "BluetoothAdapter.h"
 #include "BluetoothService.h"
 #include "BluetoothReplyRunnable.h"
 
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/dom/BluetoothManager2Binding.h"
 #include "mozilla/Services.h"
 #include "nsContentUtils.h"
@@ -71,18 +70,18 @@ class GetAdaptersTask : public Bluetooth
       const BluetoothValue& properties = adaptersPropertiesArray[i].value();
       mManager->AppendAdapter(properties);
     }
 
     aValue.setUndefined();
     return true;
   }
 
-  void
-  ReleaseMembers()
+  virtual void
+  ReleaseMembers() MOZ_OVERRIDE
   {
     BluetoothReplyRunnable::ReleaseMembers();
     mManager = nullptr;
   }
 
 private:
   nsRefPtr<BluetoothManager> mManager;
 };
--- a/dom/bluetooth2/BluetoothService.cpp
+++ b/dom/bluetooth2/BluetoothService.cpp
@@ -376,20 +376,22 @@ BluetoothService::StartBluetooth(bool aI
 
   /* When IsEnabled() is true, we don't switch on Bluetooth but we still
    * send ToggleBtAck task. One special case happens at startup stage. At
    * startup, the initialization of BluetoothService still has to be done
    * even if Bluetooth is already enabled.
    *
    * Please see bug 892392 for more information.
    */
-  if (aIsStartup || !sBluetoothService->IsEnabled()) {
+  if (aIsStartup || !IsEnabled()) {
     // Switch Bluetooth on
-    if (NS_FAILED(sBluetoothService->StartInternal(aRunnable))) {
+    nsresult rv = StartInternal(aRunnable);
+    if (NS_FAILED(rv)) {
       BT_WARNING("Bluetooth service failed to start!");
+      return rv;
     }
   } else {
     BT_WARNING("Bluetooth has already been enabled before.");
     nsRefPtr<nsRunnable> runnable = new BluetoothService::ToggleBtAck(true);
     if (NS_FAILED(NS_DispatchToMainThread(runnable))) {
       BT_WARNING("Failed to dispatch to main thread!");
     }
   }
@@ -436,20 +438,22 @@ BluetoothService::StopBluetooth(bool aIs
 
   /* When IsEnabled() is false, we don't switch off Bluetooth but we still
    * send ToggleBtAck task. One special case happens at startup stage. At
    * startup, the initialization of BluetoothService still has to be done
    * even if Bluetooth is disabled.
    *
    * Please see bug 892392 for more information.
    */
-  if (aIsStartup || sBluetoothService->IsEnabled()) {
+  if (aIsStartup || IsEnabled()) {
     // Switch Bluetooth off
-    if (NS_FAILED(sBluetoothService->StopInternal(aRunnable))) {
+    nsresult rv = StopInternal(aRunnable);
+    if (NS_FAILED(rv)) {
       BT_WARNING("Bluetooth service failed to stop!");
+      return rv;
     }
   } else {
     BT_WARNING("Bluetooth has already been enabled/disabled before.");
     nsRefPtr<nsRunnable> runnable = new BluetoothService::ToggleBtAck(false);
     if (NS_FAILED(NS_DispatchToMainThread(runnable))) {
       BT_WARNING("Failed to dispatch to main thread!");
     }
   }
--- a/dom/bluetooth2/bluedroid/BluetoothA2dpManager.cpp
+++ b/dom/bluetooth2/bluedroid/BluetoothA2dpManager.cpp
@@ -2,24 +2,18 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "base/basictypes.h"
 
 #include "BluetoothA2dpManager.h"
-
-#include <hardware/bluetooth.h>
-#include <hardware/bt_av.h>
-#if ANDROID_VERSION > 17
-#include <hardware/bt_rc.h>
-#endif
-
 #include "BluetoothCommon.h"
+#include "BluetoothInterface.h"
 #include "BluetoothService.h"
 #include "BluetoothSocket.h"
 #include "BluetoothUtils.h"
 
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "MainThreadUtils.h"
--- a/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp
+++ b/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp
@@ -27,17 +27,19 @@
 #include "BluetoothUtils.h"
 #include "BluetoothUuid.h"
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/ipc/UnixSocket.h"
 #include "mozilla/StaticMutex.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/unused.h"
 
-#define ERR_SET_PROPERTY  "SetPropertyError"
+#define ERR_SET_PROPERTY    "SetPropertyError"
+#define ERR_START_BLUETOOTH "StartBluetoothError"
+#define ERR_STOP_BLUETOOTH  "StopBluetoothError"
 
 #define ENSURE_BLUETOOTH_IS_READY(runnable, result)                    \
   do {                                                                 \
     if (!sBtInterface || !IsEnabled()) {                               \
       NS_NAMED_LITERAL_STRING(errorStr, "Bluetooth is not ready");     \
       DispatchBluetoothReply(runnable, BluetoothValue(), errorStr);    \
       return result;                                                   \
     }                                                                  \
@@ -574,17 +576,17 @@ DeviceFoundCallback(int aNumProperties, 
       BT_APPEND_NAMED_VALUE(propertiesArray, "Icon", propertyValue);
     } else {
       BT_LOGD("Not handled remote device property: %d", p.type);
     }
   }
 
   BluetoothValue value = propertiesArray;
   BluetoothSignal signal(NS_LITERAL_STRING("DeviceFound"),
-                         NS_LITERAL_STRING(KEY_ADAPTER), value);
+                         NS_LITERAL_STRING(KEY_DISCOVERY_HANDLE), value);
   nsRefPtr<DistributeBluetoothSignalTask>
     t = new DistributeBluetoothSignalTask(signal);
   if (NS_FAILED(NS_DispatchToMainThread(t))) {
     BT_WARNING("Failed to dispatch to main thread!");
   }
 }
 
 class DiscoveryStateChangedCallbackTask MOZ_FINAL : public nsRunnable
@@ -907,19 +909,28 @@ BluetoothServiceBluedroid::StartInternal
   if(aRunnable) {
     sChangeAdapterStateRunnableArray.AppendElement(aRunnable);
   }
 
   nsresult ret = StartStopGonkBluetooth(true);
   if (NS_FAILED(ret)) {
     nsRefPtr<nsRunnable> runnable =
       new BluetoothService::ToggleBtAck(false);
+
     if (NS_FAILED(NS_DispatchToMainThread(runnable))) {
       BT_WARNING("Failed to dispatch to main thread!");
     }
+
+    // Reject Promise
+    if(aRunnable) {
+      DispatchBluetoothReply(aRunnable, BluetoothValue(),
+                             NS_LITERAL_STRING(ERR_START_BLUETOOTH));
+      sChangeAdapterStateRunnableArray.RemoveElement(aRunnable);
+    }
+
     BT_LOGR("Error");
   }
 
   return ret;
 }
 
 nsresult
 BluetoothServiceBluedroid::StopInternal(BluetoothReplyRunnable* aRunnable)
@@ -930,19 +941,28 @@ BluetoothServiceBluedroid::StopInternal(
   if(aRunnable) {
     sChangeAdapterStateRunnableArray.AppendElement(aRunnable);
   }
 
   nsresult ret = StartStopGonkBluetooth(false);
   if (NS_FAILED(ret)) {
     nsRefPtr<nsRunnable> runnable =
       new BluetoothService::ToggleBtAck(true);
+
     if (NS_FAILED(NS_DispatchToMainThread(runnable))) {
       BT_WARNING("Failed to dispatch to main thread!");
     }
+
+    // Reject Promise
+    if(aRunnable) {
+      DispatchBluetoothReply(aRunnable, BluetoothValue(),
+                             NS_LITERAL_STRING(ERR_STOP_BLUETOOTH));
+      sChangeAdapterStateRunnableArray.RemoveElement(aRunnable);
+    }
+
     BT_LOGR("Error");
   }
 
   return ret;
 }
 
 nsresult
 BluetoothServiceBluedroid::GetAdaptersInternal(
--- a/dom/bluetooth2/moz.build
+++ b/dom/bluetooth2/moz.build
@@ -3,16 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 if CONFIG['MOZ_B2G_BT']:
     SOURCES += [
         'BluetoothAdapter.cpp',
         'BluetoothDevice.cpp',
+        'BluetoothDiscoveryHandle.cpp',
         'BluetoothHidManager.cpp',
         'BluetoothManager.cpp',
         'BluetoothProfileController.cpp',
         'BluetoothReplyRunnable.cpp',
         'BluetoothService.cpp',
         'BluetoothUuid.cpp',
         'ipc/BluetoothChild.cpp',
         'ipc/BluetoothParent.cpp',
@@ -89,16 +90,17 @@ if CONFIG['MOZ_B2G_BT']:
 EXPORTS.mozilla.dom.bluetooth.ipc += [
     'ipc/BluetoothMessageUtils.h',
 ]
 
 EXPORTS.mozilla.dom.bluetooth += [
     'BluetoothAdapter.h',
     'BluetoothCommon.h',
     'BluetoothDevice.h',
+    'BluetoothDiscoveryHandle.h',
     'BluetoothManager.h',
 ]
 
 IPDL_SOURCES += [
     'ipc/BluetoothTypes.ipdlh',
     'ipc/PBluetooth.ipdl',
     'ipc/PBluetoothRequest.ipdl',
 ]
--- a/dom/icc/tests/marionette/test_icc_info.js
+++ b/dom/icc/tests/marionette/test_icc_info.js
@@ -57,56 +57,16 @@ taskHelper.push(function basicTest() {
     // See it here {B2G_HOME}/hardware/ril/reference-ril/reference-ril.c,
     // in requestCdmaSubscription()
     is(iccInfo.prlVersion, 1);
   }
 
   taskHelper.runNext();
 });
 
-/* Test Gsm display condition change */
-taskHelper.push(function testGsmDisplayConditionChange() {
-  function testSPN(mcc, mnc, expectedIsDisplayNetworkNameRequired,
-                   expectedIsDisplaySpnRequired, callback) {
-    icc.addEventListener("iccinfochange", function handler() {
-      icc.removeEventListener("iccinfochange", handler);
-      is(icc.iccInfo.isDisplayNetworkNameRequired,
-         expectedIsDisplayNetworkNameRequired);
-      is(icc.iccInfo.isDisplaySpnRequired,
-         expectedIsDisplaySpnRequired);
-      // operatorchange will be ignored if we send commands too soon.
-      window.setTimeout(callback, 100);
-    });
-    // Send emulator command to change network mcc and mnc.
-    setEmulatorMccMnc(mcc, mnc);
-  }
-
-  let testCases = [
-    // [MCC, MNC, isDisplayNetworkNameRequired, isDisplaySpnRequired]
-    [123, 456, false, true], // Not in HPLMN.
-    [234, 136,  true, true], // Not in HPLMN, but in PLMN specified in SPDI.
-    [123, 456, false, true], // Not in HPLMN. Triggering iccinfochange
-    [466,  92,  true, true], // Not in HPLMN, but in another PLMN specified in SPDI.
-    [123, 456, false, true], // Not in HPLMN. Triggering iccinfochange
-    [310, 260,  true, true], // inside HPLMN.
-  ];
-
-  // Ignore this test if device is not in gsm mode.
-  if (!(icc.iccInfo instanceof Ci.nsIDOMMozGsmIccInfo)) {
-    taskHelper.runNext();
-    return;
-  }
-
-  (function do_call(index) {
-    let next = index < (testCases.length - 1) ? do_call.bind(null, index + 1) : taskHelper.runNext.bind(taskHelper);
-    testCases[index].push(next);
-    testSPN.apply(null, testCases[index]);
-  })(0);
-});
-
 /* Test iccInfo when card becomes undetected */
 taskHelper.push(function testCardIsNotReady() {
   // Turn off radio.
   setRadioEnabled(false);
   icc.addEventListener("iccinfochange", function oniccinfochange() {
     // Expect iccInfo changes to null
     if (icc.iccInfo === null) {
       icc.removeEventListener("iccinfochange", oniccinfochange);
--- a/dom/icc/tests/marionette/test_icc_match_mvno.js
+++ b/dom/icc/tests/marionette/test_icc_match_mvno.js
@@ -12,18 +12,21 @@ let testCases = [
   ["imsi", "31026xx0",           true, true               ],
   ["imsi", "310260x0x",          true, true               ],
   ["imsi", "310260X00",          true, true               ],
   ["imsi", "310260XX1",          true, false              ],
   ["imsi", "31026012",           true, false              ],
   ["imsi", "310260000000000",    true, true               ],
   ["imsi", "310260000000000123", true, false              ],
   ["imsi", "",                   false, "InvalidParameter"],
-  // Currently we only support imsi match.
-  ["spn",  "Android",            false, "ModeNotSupported"]
+  ["spn",  "Android",            true, true               ],
+  ["spn",  "",                   false, "InvalidParameter"],
+  ["spn",  "OneTwoThree",        true, false              ],
+  // mvno type gid is not supported yet.
+  ["gid",  "A1",                 false, "ModeNotSupported"]
 ];
 
 function matchMvno(mvnoType, mvnoData, success, expectedResult) {
   log("matchMvno: " + mvnoType + ", " + mvnoData);
   let request = icc.matchMvno(mvnoType, mvnoData);
   request.onsuccess = function onsuccess() {
     log("onsuccess: " + request.result);
     ok(success, "onsuccess while error expected");
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -78,16 +78,17 @@ PARALLEL_DIRS += [
     'audiochannel',
     'promise',
     'smil',
     'telephony',
     'inputmethod',
     'webidl',
     'xbl',
     'xslt',
+    'resourcestats',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     PARALLEL_DIRS += ['plugins/ipc/hangui']
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
     PARALLEL_DIRS += [
         'speakermanager',
--- a/dom/nfc/tests/marionette/test_nfc_tag.js
+++ b/dom/nfc/tests/marionette/test_nfc_tag.js
@@ -5,16 +5,17 @@ MARIONETTE_TIMEOUT = 30000;
 MARIONETTE_HEAD_JS = "head.js";
 
 let url = "http://www.mozilla.org";
 
 // TODO : Get this from emulator console command.
 const T1T_RE_INDEX = 2;
 const T2T_RE_INDEX = 3;
 const T3T_RE_INDEX = 4;
+const T4T_RE_INDEX = 5;
 
 function testUrlTagDiscover(re) {
   log("Running \'testUrlTagDiscover\'");
   // TODO : Make flag value readable.
   let flag = 0xd0;
   let tnf = NDEF.TNF_WELL_KNOWN;
   let type = "U";
   let payload = url;
@@ -47,16 +48,21 @@ function testUrlT1TDiscover() {
 function testUrlT2TDiscover() {
   testUrlTagDiscover(T2T_RE_INDEX);
 }
 
 function testUrlT3TDiscover() {
   testUrlTagDiscover(T3T_RE_INDEX);
 }
 
+function testUrlT4TDiscover() {
+  testUrlTagDiscover(T4T_RE_INDEX);
+}
+
 let tests = [
   testUrlT1TDiscover,
   testUrlT2TDiscover,
-  testUrlT3TDiscover
+  testUrlT3TDiscover,
+  testUrlT4TDiscover
 ];
 
 SpecialPowers.pushPermissions(
   [{'type': 'nfc-manager', 'allow': true, context: document}], runTests);
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsDB.jsm
@@ -0,0 +1,537 @@
+/* This Source Code Form is subject to the terms of the Mozilla public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ['ResourceStatsDB'];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsDB: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
+const DB_NAME = "resource_stats";
+const DB_VERSION = 1;
+const POWER_STATS_STORE = "power_stats_store";
+const NETWORK_STATS_STORE = "network_stats_store";
+const ALARM_STORE = "alarm_store";
+
+const statsStoreNames = {
+  power: POWER_STATS_STORE,
+  network: NETWORK_STATS_STORE
+};
+
+// Constant defining the sampling rate.
+const SAMPLE_RATE = 24 * 60 * 60 * 1000; // 1 day.
+
+// Constant defining the MAX age of stored stats.
+const MAX_STORAGE_AGE = 180 * SAMPLE_RATE; // 180 days.
+
+this.ResourceStatsDB = function ResourceStatsDB() {
+  if (DEBUG) {
+    debug("Constructor()");
+  }
+
+  this.initDBHelper(DB_NAME, DB_VERSION,
+                    [POWER_STATS_STORE, NETWORK_STATS_STORE, ALARM_STORE]);
+};
+
+ResourceStatsDB.prototype = {
+  __proto__: IndexedDBHelper.prototype,
+
+  _dbNewTxn: function(aStoreName, aTxnType, aCallback, aTxnCb) {
+    function successCb(aResult) {
+      aTxnCb(null, aResult);
+    }
+    function errorCb(aError) {
+      aTxnCb(aError, null);
+    }
+    return this.newTxn(aTxnType, aStoreName, aCallback, successCb, errorCb);
+  },
+
+  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+    if (DEBUG) {
+      debug("Upgrade DB from ver." + aOldVersion + " to ver." + aNewVersion);
+    }
+
+    let objectStore;
+
+    // Create PowerStatsStore.
+    objectStore = aDb.createObjectStore(POWER_STATS_STORE, {
+      keyPath: ["appId", "serviceType", "component", "timestamp"]
+    });
+    objectStore.createIndex("component", "component", { unique: false });
+
+    // Create NetworkStatsStore.
+    objectStore = aDb.createObjectStore(NETWORK_STATS_STORE, {
+      keyPath: ["appId", "serviceType", "component", "timestamp"]
+    });
+    objectStore.createIndex("component", "component", { unique: false });
+
+    // Create AlarmStore.
+    objectStore = aDb.createObjectStore(ALARM_STORE, {
+      keyPath: "alarmId",
+      autoIncrement: true
+    });
+    objectStore.createIndex("type", "type", { unique: false });
+    // Index for resource control target.
+    objectStore.createIndex("controlTarget",
+                            ["type", "manifestURL", "serviceType", "component"],
+                            { unique: false });
+  },
+
+  // Convert to UTC according to the current timezone and the filter timestamp
+  // to get SAMPLE_RATE precission.
+  _normalizeTime: function(aTime, aOffset) {
+    let time = Math.floor((aTime - aOffset) / SAMPLE_RATE) * SAMPLE_RATE;
+
+    return time;
+  },
+
+  /**
+   * aRecordArray contains an array of json objects storing network stats.
+   * The structure of the json object =
+   *  {
+   *    appId: XX,
+   *    serviceType: "XX",
+   *    componentStats: {
+   *      "component_1": { receivedBytes: XX, sentBytes: XX },
+   *      "component_2": { receivedBytes: XX, sentBytes: XX },
+   *      ...
+   *    },
+   *  }
+   */
+  saveNetworkStats: function(aRecordArray, aTimestamp, aResultCb) {
+    if (DEBUG) {
+      debug("saveNetworkStats()");
+    }
+
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let timestamp = this._normalizeTime(aTimestamp, offset);
+
+    this._dbNewTxn(NETWORK_STATS_STORE, "readwrite", function(aTxn, aStore) {
+      aRecordArray.forEach(function(aRecord) {
+        let stats = {
+          appId: aRecord.appId,
+          serviceType: aRecord.serviceType,
+          component: "",
+          timestamp: timestamp,
+          receivedBytes: 0,
+          sentBytes: 0
+        };
+
+        let totalReceivedBytes = 0;
+        let totalSentBytes = 0;
+
+        // Save stats of each component.
+        let data = aRecord.componentStats;
+        for (let component in data) {
+          // Save stats to database.
+          stats.component = component;
+          stats.receivedBytes = data[component].receivedBytes;
+          stats.sentBytes = data[component].sentBytes;
+          aStore.put(stats);
+          if (DEBUG) {
+            debug("Save network stats: " + JSON.stringify(stats));
+          }
+
+          // Accumulated to tatal stats.
+          totalReceivedBytes += stats.receivedBytes;
+          totalSentBytes += stats.sentBytes;
+        }
+
+        // Save total stats.
+        stats.component = "";
+        stats.receivedBytes = totalReceivedBytes;
+        stats.sentBytes = totalSentBytes;
+        aStore.put(stats);
+        if (DEBUG) {
+          debug("Save network stats: " + JSON.stringify(stats));
+        }
+      });
+    }, aResultCb);
+  },
+
+  /**
+   * aRecordArray contains an array of json objects storing power stats.
+   * The structure of the json object =
+   *  {
+   *    appId: XX,
+   *    serviceType: "XX",
+   *    componentStats: {
+   *      "component_1": XX, // consumedPower
+   *      "component_2": XX,
+   *      ...
+   *    },
+   *  }
+   */
+  savePowerStats: function(aRecordArray, aTimestamp, aResultCb) {
+    if (DEBUG) {
+      debug("savePowerStats()");
+    }
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let timestamp = this._normalizeTime(aTimestamp, offset);
+
+    this._dbNewTxn(POWER_STATS_STORE, "readwrite", function(aTxn, aStore) {
+      aRecordArray.forEach(function(aRecord) {
+        let stats = {
+          appId: aRecord.appId,
+          serviceType: aRecord.serviceType,
+          component: "",
+          timestamp: timestamp,
+          consumedPower: aRecord.totalStats
+        };
+
+        let totalConsumedPower = 0;
+
+        // Save stats of each component to database.
+        let data = aRecord.componentStats;
+        for (let component in data) {
+          // Save stats to database.
+          stats.component = component;
+          stats.consumedPower = data[component];
+          aStore.put(stats);
+          if (DEBUG) {
+            debug("Save power stats: " + JSON.stringify(stats));
+          }
+          // Accumulated to total stats.
+          totalConsumedPower += stats.consumedPower;
+        }
+
+        // Save total stats.
+        stats.component = "";
+        stats.consumedPower = totalConsumedPower;
+        aStore.put(stats);
+        if (DEBUG) {
+          debug("Save power stats: " + JSON.stringify(stats));
+        }
+      });
+    }, aResultCb);
+  },
+
+  // Get stats from a store.
+  getStats: function(aType, aManifestURL, aServiceType, aComponent,
+                     aStart, aEnd, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.getStats()");
+    }
+
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+    // Get appId and check whether manifestURL is a valid app.
+    let appId = 0;
+    if (aManifestURL) {
+      appId = appsService.getAppLocalIdByManifestURL(aManifestURL);
+
+      if (!appId) {
+        aResultCb("Invalid manifestURL", null);
+        return;
+      }
+    }
+
+    // Get store name.
+    let storeName = statsStoreNames[aType];
+
+    // Normalize start time and end time to SAMPLE_RATE precission.
+    let start = this._normalizeTime(aStart, offset);
+    let end = this._normalizeTime(aEnd, offset);
+    if (DEBUG) {
+      debug("Query time range: " + start + " to " + end);
+      debug("[appId, serviceType, component] = [" + appId + ", " + aServiceType
+            + ", " + aComponent + "]");
+    }
+
+    // Create filters.
+    let lowerFilter = [appId, aServiceType, aComponent, start];
+    let upperFilter = [appId, aServiceType, aComponent, end];
+
+    // Execute DB query.
+    this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
+      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+
+      let statsData = [];
+
+      if (!aTxn.result) {
+        aTxn.result = Object.create(null);
+      }
+      aTxn.result.type = aType;
+      aTxn.result.component = aComponent;
+      aTxn.result.serviceType = aServiceType;
+      aTxn.result.manifestURL = aManifestURL;
+      aTxn.result.start = start + offset;
+      aTxn.result.end = end + offset;
+      // Since ResourceStats() would require SAMPLE_RATE when filling the empty
+      // entries of statsData array, we append SAMPLE_RATE to the result field
+      // to save an IPC call.
+      aTxn.result.sampleRate = SAMPLE_RATE;
+
+      let request = aStore.openCursor(range, "prev");
+      if (aType == "power") {
+        request.onsuccess = function(aEvent) {
+          var cursor = aEvent.target.result;
+          if (cursor) {
+            if (DEBUG) {
+              debug("Get " + JSON.stringify(cursor.value));
+            }
+
+            // Covert timestamp to current timezone.
+            statsData.push({
+              timestamp: cursor.value.timestamp + offset,
+              consumedPower: cursor.value.consumedPower
+            });
+            cursor.continue();
+            return;
+          }
+          aTxn.result.statsData = statsData;
+        };
+      } else if (aType == "network") {
+        request.onsuccess = function(aEvent) {
+          var cursor = aEvent.target.result;
+          if (cursor) {
+            if (DEBUG) {
+              debug("Get " + JSON.stringify(cursor.value));
+            }
+
+            // Covert timestamp to current timezone.
+            statsData.push({
+              timestamp: cursor.value.timestamp + offset,
+              receivedBytes: cursor.value.receivedBytes,
+              sentBytes: cursor.value.sentBytes
+            });
+            cursor.continue();
+            return;
+          }
+          aTxn.result.statsData = statsData;
+        };
+      }
+    }, aResultCb);
+  },
+
+  // Delete the stats of a specific app/service (within a specified time range).
+  clearStats: function(aType, aAppId, aServiceType, aComponent,
+                       aStart, aEnd, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.clearStats()");
+    }
+
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+    // Get store name.
+    let storeName = statsStoreNames[aType];
+
+    // Normalize start and end time to SAMPLE_RATE precission.
+    let start = this._normalizeTime(aStart, offset);
+    let end = this._normalizeTime(aEnd, offset);
+    if (DEBUG) {
+      debug("Query time range: " + start + " to " + end);
+      debug("[appId, serviceType, component] = [" + aAppId + ", " + aServiceType
+            + ", " + aComponent + "]");
+    }
+
+    // Create filters.
+    let lowerFilter = [aAppId, aServiceType, aComponent, start];
+    let upperFilter = [aAppId, aServiceType, aComponent, end];
+
+    // Execute clear operation.
+    this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
+      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+      let request = aStore.openCursor(range).onsuccess = function(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          if (DEBUG) {
+            debug("Delete " + JSON.stringify(cursor.value) + " from database");
+          }
+          cursor.delete();
+          cursor.continue();
+          return;
+        }
+      };
+    }, aResultCb);
+  },
+
+  // Delete all stats saved in a store.
+  clearAllStats: function(aType, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.clearAllStats()");
+    }
+
+    let storeName = statsStoreNames[aType];
+
+    // Execute clear operation.
+    this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
+      if (DEBUG) {
+        debug("Clear " + aType + " stats from datastore");
+      }
+      aStore.clear();
+    }, aResultCb);
+  },
+
+  addAlarm: function(aAlarm, aResultCb) {
+    if (DEBUG) {
+      debug(aAlarm.type + "Mgr.addAlarm()");
+      debug("alarm = " + JSON.stringify(aAlarm));
+    }
+
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      aStore.put(aAlarm).onsuccess = function setResult(aEvent) {
+        // Get alarmId.
+        aTxn.result = aEvent.target.result;
+        if (DEBUG) {
+          debug("New alarm ID: " + aTxn.result);
+        }
+      };
+    }, aResultCb);
+  },
+
+  // Convert DB record to alarm object.
+  _recordToAlarm: function(aRecord) {
+    let alarm = {
+      alarmId: aRecord.alarmId,
+      type: aRecord.type,
+      component: aRecord.component,
+      serviceType: aRecord.serviceType,
+      manifestURL: aRecord.manifestURL,
+      threshold: aRecord.threshold,
+      data: aRecord.data
+    };
+
+    return alarm;
+  },
+
+  getAlarms: function(aType, aOptions, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.getAlarms()");
+      debug("[appId, serviceType, component] = [" + aOptions.appId + ", "
+            + aOptions.serviceType + ", " + aOptions.component + "]");
+    }
+
+    // Execute clear operation.
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      if (!aTxn.result) {
+        aTxn.result = [];
+      }
+
+      let indexName = null;
+      let range = null;
+
+      if (aOptions) { // Get alarms associated to specified statsOptions.
+        indexName = "controlTarget";
+        range = IDBKeyRange.only([aType, aOptions.manifestURL,
+                                  aOptions.serviceType, aOptions.component]);
+      } else { // Get all alarms of the specified type.
+        indexName = "type";
+        range = IDBKeyRange.only(aType);
+      }
+
+      let request = aStore.index(indexName).openCursor(range);
+      request.onsuccess = function onsuccess(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          aTxn.result.push(this._recordToAlarm(cursor.value));
+          cursor.continue();
+          return;
+        }
+      }.bind(this);
+    }.bind(this), aResultCb);
+  },
+
+  removeAlarm: function(aType, aAlarmId, aResultCb) {
+    if (DEBUG) {
+      debug("removeAlarms(" + aAlarmId + ")");
+    }
+
+    // Execute clear operation.
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      aStore.get(aAlarmId).onsuccess = function onsuccess(aEvent) {
+        let alarm = aEvent.target.result;
+        aTxn.result = false;
+        if (!alarm || aType !== alarm.type) {
+          return;
+        }
+
+        if (DEBUG) {
+          debug("Remove alarm " + JSON.stringify(alarm) + " from datastore");
+        }
+        aStore.delete(aAlarmId);
+        aTxn.result = true;
+      };
+    }, aResultCb);
+  },
+
+  removeAllAlarms: function(aType, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.removeAllAlarms()");
+    }
+
+    // Execute clear operation.
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      if (DEBUG) {
+        debug("Remove all " + aType + " alarms from datastore.");
+      }
+
+      let range = IDBKeyRange.only(aType);
+      let request = aStore.index("type").openCursor(range);
+      request.onsuccess = function onsuccess(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          if (DEBUG) {
+            debug("Remove " + JSON.stringify(cursor.value) + " from database.");
+          }
+          cursor.delete();
+          cursor.continue();
+          return;
+        }
+      };
+    }, aResultCb);
+  },
+
+  // Get all index keys of the component.
+  getComponents: function(aType, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.getComponents()");
+    }
+
+    let storeName = statsStoreNames[aType];
+
+    this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
+      if (!aTxn.result) {
+        aTxn.result = [];
+      }
+
+      let request = aStore.index("component").openKeyCursor(null, "nextunique");
+      request.onsuccess = function onsuccess(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          aTxn.result.push(cursor.key);
+          cursor.continue();
+          return;
+        }
+
+        // Remove "" from the result, which indicates sum of all
+        // components' stats.
+        let index = aTxn.result.indexOf("");
+        if (index > -1) {
+          aTxn.result.splice(index, 1);
+        }
+      };
+    }, aResultCb);
+  },
+
+  get sampleRate () {
+    return SAMPLE_RATE;
+  },
+
+  get maxStorageAge() {
+    return MAX_STORAGE_AGE;
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsManager.js
@@ -0,0 +1,480 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsManager: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+// Constant defines supported statistics.
+const resourceTypeList = ["network", "power"];
+
+function NetworkStatsData(aStatsData) {
+  if (DEBUG) {
+    debug("NetworkStatsData(): " + JSON.stringify(aStatsData));
+  }
+
+  this.receivedBytes = aStatsData.receivedBytes || 0;
+  this.sentBytes = aStatsData.sentBytes || 0;
+  this.timestamp = aStatsData.timestamp;
+}
+
+NetworkStatsData.prototype = {
+  classID: Components.ID("{dce5729a-ba92-4185-8854-e29e71b9e8a2}"),
+  contractID: "@mozilla.org/networkStatsData;1",
+  QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function PowerStatsData(aStatsData) {
+  if (DEBUG) {
+    debug("PowerStatsData(): " + JSON.stringify(aStatsData));
+  }
+
+  this.consumedPower = aStatsData.consumedPower || 0;
+  this.timestamp = aStatsData.timestamp;
+}
+
+PowerStatsData.prototype = {
+  classID: Components.ID("{acb9af6c-8143-4e59-bc18-4bb1736a4004}"),
+  contractID: "@mozilla.org/powerStatsData;1",
+  QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function ResourceStats(aWindow, aStats) {
+  if (DEBUG) {
+    debug("ResourceStats(): " + JSON.stringify(aStats));
+  }
+
+  this._window = aWindow;
+  this.type = aStats.type;
+  this.component = aStats.component || null;
+  this.serviceType = aStats.serviceType || null;
+  this.manifestURL = aStats.manifestURL || null;
+  this.start = aStats.start;
+  this.end = aStats.end;
+  this.statsData = new aWindow.Array();
+
+  // A function creates a StatsData object according to type.
+  let createStatsDataObject = null;
+  let self = this;
+  switch (this.type) {
+    case "power":
+      createStatsDataObject = function(aStats) {
+        let chromeObj = new PowerStatsData(aStats);
+        return self._window.PowerStatsData._create(self._window, chromeObj);
+      };
+      break;
+    case "network":
+      createStatsDataObject = function(aStats) {
+        let chromeObj = new NetworkStatsData(aStats);
+        return self._window.NetworkStatsData._create(self._window, chromeObj);
+      };
+      break;
+  }
+
+  let sampleRate = aStats.sampleRate;
+  let queryResult = aStats.statsData;
+  let stats = queryResult.pop(); // Pop out the last element.
+  let timestamp = this.start;
+
+  // Push query result to statsData, and fill empty elements so that:
+  // 1. the timestamp of the first element of statsData is equal to start;
+  // 2. the timestamp of the last element of statsData is equal to end;
+  // 3. the timestamp difference of every neighboring elements is SAMPLE_RATE.
+  for (; timestamp <= this.end; timestamp += sampleRate) {
+    if (!stats || stats.timestamp != timestamp) {
+      // If dataArray is empty or timestamp are not equal, push a dummy object
+      // (which stats are set to 0) to statsData.
+      this.statsData.push(createStatsDataObject({ timestamp: timestamp }));
+    } else {
+      // Push stats to statsData and pop a new element form queryResult.
+      this.statsData.push(createStatsDataObject(stats));
+      stats = queryResult.pop();
+    }
+  }
+}
+
+ResourceStats.prototype = {
+  getData: function() {
+    return this.statsData;
+  },
+
+  classID: Components.ID("{b7c970f2-3d58-4966-9633-2024feb5132b}"),
+  contractID: "@mozilla.org/resourceStats;1",
+  QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function ResourceStatsAlarm(aWindow, aAlarm) {
+  if (DEBUG) {
+    debug("ResourceStatsAlarm(): " + JSON.stringify(aAlarm));
+  }
+
+  this.alarmId = aAlarm.alarmId;
+  this.type = aAlarm.type;
+  this.component = aAlarm.component || null;
+  this.serviceType = aAlarm.serviceType || null;
+  this.manifestURL = aAlarm.manifestURL || null;
+  this.threshold = aAlarm.threshold;
+
+  // Clone data object using structured clone algorithm.
+  this.data = null;
+  if (aAlarm.data) {
+    this.data = Cu.cloneInto(aAlarm.data, aWindow);
+  }
+}
+
+ResourceStatsAlarm.prototype = {
+  classID: Components.ID("{e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}"),
+  contractID: "@mozilla.org/resourceStatsAlarm;1",
+  QueryInterface: XPCOMUtils.generateQI([]),
+};
+
+function ResourceStatsManager() {
+  if (DEBUG) {
+    debug("constructor()");
+  }
+}
+
+ResourceStatsManager.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  _getPromise: function(aCallback) {
+    let self = this;
+    return this.createPromise(function(aResolve, aReject) {
+      let resolverId = self.getPromiseResolverId({
+        resolve: aResolve,
+        reject: aReject
+      });
+
+      aCallback(resolverId);
+    });
+  },
+
+  // Check time range.
+  _checkTimeRange: function(aStart, aEnd) {
+    if (DEBUG) {
+      debug("_checkTimeRange(): " + aStart + " to " + aEnd);
+    }
+
+    let start = aStart ? aStart : 0;
+    let end = aEnd ? aEnd : Date.now();
+
+    if (start > end) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    return { start: start, end: end };
+  },
+
+  getStats: function(aStatsOptions, aStart, aEnd) {
+    // Check time range.
+    let { start: start, end: end } = this._checkTimeRange(aStart, aEnd);
+
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:GetStats", {
+        resolverId: aResolverId,
+        type: self.type,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL,
+        start: start,
+        end: end
+      });
+    });
+  },
+
+  clearStats: function(aStatsOptions, aStart, aEnd) {
+    // Check time range.
+    let {start: start, end: end} = this._checkTimeRange(aStart, aEnd);
+
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:ClearStats", {
+        resolverId: aResolverId,
+        type: self.type,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL,
+        start: start,
+        end: end
+      });
+    });
+  },
+
+  clearAllStats: function() {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:ClearAllStats", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  addAlarm: function(aThreshold, aStatsOptions, aAlarmOptions) {
+    if (DEBUG) {
+      debug("aStatsOptions: " + JSON.stringify(aAlarmOptions));
+      debug("aAlarmOptions: " + JSON.stringify(aAlarmOptions));
+    }
+
+    // Parse alarm options.
+    let startTime = aAlarmOptions.startTime || 0;
+
+    // Clone data object using structured clone algorithm.
+    let data = null;
+    if (aAlarmOptions.data) {
+      data = Cu.cloneInto(aAlarmOptions.data, this._window);
+    }
+
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:AddAlarm", {
+        resolverId: aResolverId,
+        type: self.type,
+        threshold: aThreshold,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL,
+        startTime: startTime,
+        data: data
+      });
+    });
+  },
+
+  getAlarms: function(aStatsOptions) {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:GetAlarms", {
+        resolverId: aResolverId,
+        type: self.type,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  removeAlarm: function(aAlarmId) {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:RemoveAlarm", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL,
+        alarmId: aAlarmId
+      });
+    });
+  },
+
+  removeAllAlarms: function() {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:RemoveAllAlarms", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  getAvailableComponents: function() {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:GetComponents", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  get resourceTypes() {
+    let types = new this._window.Array();
+    resourceTypeList.forEach(function(aType) {
+      types.push(aType);
+    });
+
+    return types;
+  },
+
+  get sampleRate() {
+    let msg = { manifestURL: this._manifestURL };
+
+    return this.cpmm.sendSyncMessage("ResourceStats:SampleRate", msg)[0];
+  },
+
+  get maxStorageAge() {
+    let msg = { manifestURL: this._manifestURL };
+
+    return this.cpmm.sendSyncMessage("ResourceStats:MaxStorageAge", msg)[0];
+  },
+
+  receiveMessage: function(aMessage) {
+    let data = aMessage.data;
+    let chromeObj = null;
+    let webidlObj = null;
+    let self = this;
+
+    if (DEBUG) {
+      debug("receiveMessage(): " + aMessage.name + " " + data.resolverId);
+    }
+
+    let resolver = this.takePromiseResolver(data.resolverId);
+    if (!resolver) {
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "ResourceStats:GetStats:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        chromeObj = new ResourceStats(this._window, data.value);
+        webidlObj = this._window.ResourceStats._create(this._window, chromeObj);
+        resolver.resolve(webidlObj);
+        break;
+
+      case "ResourceStats:AddAlarm:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        resolver.resolve(data.value); // data.value is alarmId.
+        break;
+
+      case "ResourceStats:GetAlarms:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        let alarmArray = this._window.Array();
+        data.value.forEach(function(aAlarm) {
+          chromeObj = new ResourceStatsAlarm(self._window, aAlarm);
+          webidlObj = self._window.ResourceStatsAlarm._create(self._window,
+                                                              chromeObj);
+          alarmArray.push(webidlObj);
+        });
+        resolver.resolve(alarmArray);
+        break;
+
+      case "ResourceStats:GetComponents:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        let components = this._window.Array();
+        data.value.forEach(function(aComponent) {
+          components.push(aComponent);
+        });
+        resolver.resolve(components);
+        break;
+
+      case "ResourceStats:ClearStats:Resolved":
+      case "ResourceStats:ClearAllStats:Resolved":
+      case "ResourceStats:RemoveAlarm:Resolved":
+      case "ResourceStats:RemoveAllAlarms:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        resolver.resolve(data.value);
+        break;
+
+      case "ResourceStats:GetStats:Rejected":
+      case "ResourceStats:ClearStats:Rejected":
+      case "ResourceStats:ClearAllStats:Rejected":
+      case "ResourceStats:AddAlarm:Rejected":
+      case "ResourceStats:GetAlarms:Rejected":
+      case "ResourceStats:RemoveAlarm:Rejected":
+      case "ResourceStats:RemoveAllAlarms:Rejected":
+      case "ResourceStats:GetComponents:Rejected":
+        if (DEBUG) {
+          debug("data.reason = " + JSON.stringify(data.reason));
+        }
+
+        resolver.reject(data.reason);
+        break;
+
+      default:
+        if (DEBUG) {
+          debug("Could not find a handler for " + aMessage.name);
+        }
+        resolver.reject();
+    }
+  },
+
+  __init: function(aType) {
+    if (resourceTypeList.indexOf(aType) < 0) {
+      if (DEBUG) {
+        debug("Do not support resource statistics for " + aType);
+      }
+      throw Cr.NS_ERROR_INVALID_ARG;
+    } else {
+      if (DEBUG) {
+        debug("Create " + aType + "Mgr");
+      }
+      this.type = aType;
+    }
+  },
+
+  init: function(aWindow) {
+    if (DEBUG) {
+      debug("init()");
+    }
+
+    // Get the manifestURL if this is an installed app
+    let principal = aWindow.document.nodePrincipal;
+    let appsService = Cc["@mozilla.org/AppsService;1"]
+                        .getService(Ci.nsIAppsService);
+    this._manifestURL = appsService.getManifestURLByLocalId(principal.appId);
+
+    const messages = ["ResourceStats:GetStats:Resolved",
+                      "ResourceStats:ClearStats:Resolved",
+                      "ResourceStats:ClearAllStats:Resolved",
+                      "ResourceStats:AddAlarm:Resolved",
+                      "ResourceStats:GetAlarms:Resolved",
+                      "ResourceStats:RemoveAlarm:Resolved",
+                      "ResourceStats:RemoveAllAlarms:Resolved",
+                      "ResourceStats:GetComponents:Resolved",
+                      "ResourceStats:GetStats:Rejected",
+                      "ResourceStats:ClearStats:Rejected",
+                      "ResourceStats:ClearAllStats:Rejected",
+                      "ResourceStats:AddAlarm:Rejected",
+                      "ResourceStats:GetAlarms:Rejected",
+                      "ResourceStats:RemoveAlarm:Rejected",
+                      "ResourceStats:RemoveAllAlarms:Rejected",
+                      "ResourceStats:GetComponents:Rejected"];
+    this.initDOMRequestHelper(aWindow, messages);
+
+    this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+                  .getService(Ci.nsISyncMessageSender);
+  },
+
+  classID: Components.ID("{101ed1f8-31b3-491c-95ea-04091e6e8027}"),
+  contractID: "@mozilla.org/resourceStatsManager;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsData,
+                                                     PowerStatsData,
+                                                     ResourceStats,
+                                                     ResourceStatsAlarm,
+                                                     ResourceStatsManager]);
+
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsManager.manifest
@@ -0,0 +1,14 @@
+component {dce5729a-ba92-4185-8854-e29e71b9e8a2} ResourceStatsManager.js
+contract @mozilla.org/networkStatsData;1 {dce5729a-ba92-4185-8854-e29e71b9e8a2}
+
+component {acb9af6c-8143-4e59-bc18-4bb1736a4004} ResourceStatsManager.js
+contract @mozilla.org/powerStatsData;1 {acb9af6c-8143-4e59-bc18-4bb1736a4004}
+
+component {b7c970f2-3d58-4966-9633-2024feb5132b} ResourceStatsManager.js
+contract @mozilla.org/resourceStats;1 {b7c970f2-3d58-4966-9633-2024feb5132b}
+
+component {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a} ResourceStatsManager.js
+contract @mozilla.org/resourceStatsAlarm;1 {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}
+
+component {101ed1f8-31b3-491c-95ea-04091e6e8027} ResourceStatsManager.js
+contract @mozilla.org/resourceStatsManager;1 {101ed1f8-31b3-491c-95ea-04091e6e8027}
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsService.jsm
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ResourceStatsService"];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsService: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+// Load ResourceStatsDB.
+Cu.import("resource://gre/modules/ResourceStatsDB.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gIDBManager",
+                                   "@mozilla.org/dom/indexeddb/manager;1",
+                                   "nsIIndexedDatabaseManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
+this.ResourceStatsService = {
+
+  init: function() {
+    if (DEBUG) {
+      debug("Service started");
+    }
+
+    // Set notification to observe.
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+
+    // Add message listener.
+    this.messages = ["ResourceStats:GetStats",
+                     "ResourceStats:ClearStats",
+                     "ResourceStats:ClearAllStats",
+                     "ResourceStats:AddAlarm",
+                     "ResourceStats:GetAlarms",
+                     "ResourceStats:RemoveAlarm",
+                     "ResourceStats:RemoveAllAlarms",
+                     "ResourceStats:GetComponents",
+                     "ResourceStats:SampleRate",
+                     "ResourceStats:MaxStorageAge"];
+
+    this.messages.forEach(function(aMessageName){
+      ppmm.addMessageListener(aMessageName, this);
+    }, this);
+
+    // Create indexedDB.
+    this._db = new ResourceStatsDB();
+  },
+
+  receiveMessage: function(aMessage) {
+    if (DEBUG) {
+      debug("receiveMessage(): " + aMessage.name);
+    }
+
+    let mm = aMessage.target;
+    let data = aMessage.data;
+
+    if (DEBUG) {
+      debug("received aMessage.data = " + JSON.stringify(data));
+    }
+
+    // To prevent the hacked child process from sending commands to parent,
+    // we need to check its permission and manifest URL.
+    if (!aMessage.target.assertPermission("resourcestats-manage")) {
+      return;
+    }
+    if (!aMessage.target.assertContainApp(data.manifestURL)) {
+      if (DEBUG) {
+        debug("Got msg from a child process containing illegal manifestURL.");
+      }
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "ResourceStats:GetStats":
+        this.getStats(mm, data);
+        break;
+      case "ResourceStats:ClearStats":
+        this.clearStats(mm, data);
+        break;
+      case "ResourceStats:ClearAllStats":
+        this.clearAllStats(mm, data);
+        break;
+      case "ResourceStats:AddAlarm":
+        this.addAlarm(mm, data);
+        break;
+      case "ResourceStats:GetAlarms":
+        this.getAlarms(mm, data);
+        break;
+      case "ResourceStats:RemoveAlarm":
+        this.removeAlarm(mm, data);
+        break;
+      case "ResourceStats:RemoveAllAlarms":
+        this.removeAllAlarms(mm, data);
+        break;
+      case "ResourceStats:GetComponents":
+        this.getComponents(mm, data);
+        break;
+      case "ResourceStats:SampleRate":
+        // This message is sync.
+        return this._db.sampleRate;
+      case "ResourceStats:MaxStorageAge":
+        // This message is sync.
+        return this._db.maxStorageAge;
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "xpcom-shutdown":
+        if (DEBUG) {
+          debug("Service shutdown " + aData);
+        }
+
+        this.messages.forEach(function(aMessageName) {
+          ppmm.removeMessageListener(aMessageName, this);
+        }, this);
+
+        Services.obs.removeObserver(this, "xpcom-shutdown");
+        break;
+      default:
+        return;
+    }
+  },
+
+  // Closure generates callback function for DB request.
+  _createDbCallback: function(aMm, aId, aMessage) {
+    let resolveMsg = aMessage + ":Resolved";
+    let rejectMsg = aMessage + ":Rejected";
+
+    return function(aError, aResult) {
+      if (aError) {
+        aMm.sendAsyncMessage(rejectMsg, { resolverId: aId, reason: aError });
+        return;
+      }
+      aMm.sendAsyncMessage(resolveMsg, { resolverId: aId, value: aResult });
+    };
+  },
+
+  getStats: function(aMm, aData) {
+    if (DEBUG) {
+      debug("getStats(): " + JSON.stringify(aData));
+    }
+
+    // Note: we validate the manifestURL in _db.getStats().
+    let manifestURL = aData.statsOptions.manifestURL || "";
+    let serviceType = aData.statsOptions.serviceType || "";
+    let component = aData.statsOptions.component || "";
+
+    // Execute DB operation.
+    let onStatsGot = this._createDbCallback(aMm, aData.resolverId,
+                                            "ResourceStats:GetStats");
+    this._db.getStats(aData.type, manifestURL, serviceType, component,
+                      aData.start, aData.end, onStatsGot);
+  },
+
+  clearStats: function(aMm, aData) {
+    if (DEBUG) {
+      debug("clearStats(): " + JSON.stringify(aData));
+    }
+
+    // Get appId and check whether manifestURL is a valid app.
+    let appId = 0;
+    let manifestURL = aData.statsOptions.manifestURL || "";
+    if (manifestURL) {
+      appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+      if (!appId) {
+        aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+          resolverId: aData.resolverId,
+          reason: "Invalid manifestURL"
+        });
+        return;
+      }
+    }
+
+    let serviceType = aData.statsOptions.serviceType || "";
+    let component = aData.statsOptions.component || "";
+
+    // Execute DB operation.
+    let onStatsCleared = this._createDbCallback(aMm, aData.resolverId,
+                                                "ResourceStats:ClearStats");
+    this._db.clearStats(aData.type, appId, serviceType, component,
+                        aData.start, aData.end, onStatsCleared);
+  },
+
+  clearAllStats: function(aMm, aData) {
+    if (DEBUG) {
+      debug("clearAllStats(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onAllStatsCleared = this._createDbCallback(aMm, aData.resolverId,
+                                                   "ResourceStats:ClearAllStats");
+    this._db.clearAllStats(aData.type, onAllStatsCleared);
+  },
+
+  addAlarm: function(aMm, aData) {
+    if (DEBUG) {
+      debug("addAlarm(): " + JSON.stringify(aData));
+    }
+
+    // Get appId and check whether manifestURL is a valid app.
+    let manifestURL = aData.statsOptions.manifestURL;
+    if (manifestURL) {
+      let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+      if (!appId) {
+        aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+          resolverId: aData.resolverId,
+          reason: "Invalid manifestURL"
+        });
+        return;
+      }
+    }
+
+    // Create an alarm object.
+    let newAlarm = {
+      type: aData.type,
+      component: aData.statsOptions.component || "",
+      serviceType: aData.statsOptions.serviceType || "",
+      manifestURL: manifestURL || "",
+      threshold: aData.threshold,
+      startTime: aData.startTime,
+      data: aData.data
+    };
+
+    // Execute DB operation.
+    let onAlarmAdded = this._createDbCallback(aMm, aData.resolverId,
+                                              "ResourceStats:AddAlarm");
+    this._db.addAlarm(newAlarm, onAlarmAdded);
+  },
+
+  getAlarms: function(aMm, aData) {
+    if (DEBUG) {
+      debug("getAlarms(): " + JSON.stringify(aData));
+    }
+
+    let options = null;
+    let statsOptions = aData.statsOptions;
+    // If all keys in statsOptions are set to null, treat this call as quering
+    // all alarms; otherwise, resolve the statsOptions and perform DB query
+    // according to the resolved result.
+    if (statsOptions.manifestURL || statsOptions.serviceType ||
+        statsOptions.component) {
+      // Validate manifestURL.
+      let manifestURL = statsOptions.manifestURL || "";
+      if (manifestURL) {
+        let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+        if (!appId) {
+          aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+            resolverId: aData.resolverId,
+            reason: "Invalid manifestURL"
+          });
+          return;
+        }
+      }
+
+      options = {
+        manifestURL: manifestURL,
+        serviceType: statsOptions.serviceType || "",
+        component: statsOptions.component || ""
+      };
+    }
+
+    // Execute DB operation.
+    let onAlarmsGot = this._createDbCallback(aMm, aData.resolverId,
+                                            "ResourceStats:GetAlarms");
+    this._db.getAlarms(aData.type, options, onAlarmsGot);
+  },
+
+  removeAlarm: function(aMm, aData) {
+    if (DEBUG) {
+      debug("removeAlarm(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onAlarmRemoved = function(aError, aResult) {
+      if (aError) {
+        aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected",
+                             { resolverId: aData.resolverId, reason: aError });
+      }
+
+      if (!aResult) {
+        aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected",
+                             { resolverId: aData.resolverId,
+                               reason: "alarm not existed" });
+      }
+
+      aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Resolved",
+                           { resolverId: aData.resolverId, value: aResult });
+    };
+
+    this._db.removeAlarm(aData.type, aData.alarmId, onAlarmRemoved);
+  },
+
+  removeAllAlarms: function(aMm, aData) {
+    if (DEBUG) {
+      debug("removeAllAlarms(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onAllAlarmsRemoved = this._createDbCallback(aMm, aData.resolverId,
+                                                    "ResourceStats:RemoveAllAlarms");
+    this._db.removeAllAlarms(aData.type, onAllAlarmsRemoved);
+  },
+
+  getComponents: function(aMm, aData) {
+    if (DEBUG) {
+      debug("getComponents(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onComponentsGot = this._createDbCallback(aMm, aData.resolverId,
+                                                 "ResourceStats:GetComponents");
+    this._db.getComponents(aData.type, onComponentsGot);
+  },
+};
+
+this.ResourceStatsService.init();
+
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_COMPONENTS += [
+    'ResourceStatsManager.js',
+    'ResourceStatsManager.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'ResourceStatsDB.jsm',
+    'ResourceStatsService.jsm',
+]
+
+TEST_DIRS += ['tests']
+
+FINAL_LIBRARY = 'gklayout'
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/mochitest.ini
@@ -0,0 +1,16 @@
+[test_basic.html]
+skip-if = toolkit != "gonk"
+[test_network_stats.html]
+skip-if = toolkit != "gonk"
+[test_power_stats.html]
+skip-if = toolkit != "gonk"
+[test_network_alarm.html]
+skip-if = toolkit != "gonk"
+[test_power_alarm.html]
+skip-if = toolkit != "gonk"
+[test_disabled_pref.html]
+skip-if = toolkit != "gonk"
+[test_no_perm.html]
+skip-if = toolkit != "gonk"
+[test_not_supported_type.html]
+skip-if = toolkit != "gonk"
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_basic.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test accessibility of interfaces</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test accessibility of interfaces.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, function() {
+  ok(SpecialPowers.hasPermission("resourcestats-manage", document),
+     "Has permission 'resourcestats-manage'.");
+  ok(SpecialPowers.getBoolPref("dom.resource_stats.enabled"),
+     "Preference 'dom.resource_stats.enabled' is true.");
+
+  // Check all interfaces are accessible.
+  ok('ResourceStatsManager' in window, "ResourceStatsManager exist.");
+  ok('ResourceStatsAlarm' in window, "ResourceStatsAlarm exist.");
+  ok('ResourceStats' in window, "ResourceStats exist.");
+  ok('NetworkStatsData' in window, "NetworkStatsData exist.");
+  ok('PowerStatsData' in window, "PowerStatsData exist.");
+
+  SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_disabled_pref.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test to ensure interface is not accessible when preference is disabled</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to ensure interface is not accessible when preference is disabled.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", false],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, function() {
+  ok(SpecialPowers.hasPermission("resourcestats-manage", document),
+     "Has permission 'resourcestats-manage'.");
+  ok(!(SpecialPowers.getBoolPref("dom.resource_stats.enabled")),
+     "Preference 'dom.resource_stats.enabled' is false.");
+
+  // Check accessibility.
+  is('ResourceStatsManager' in window, false, "ResourceStatsManager should not exist.");
+  is('ResourceStatsAlarm' in window, false, "ResourceStatsAlarm should not exist.");
+  is('ResourceStats' in window, false, "ResourceStats should not exist.");
+  is('NetworkStatsData' in window, false, "NetworkStatsData should not exist.");
+  is('PowerStatsData' in window, false, "PowerStatsData should not exist.");
+
+  SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_network_alarm.html
@@ -0,0 +1,356 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for ResourceStats methods realted to network resource control</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+const wifiComponent = "wifi:0";
+const mobileComponent = "mobile:1";
+var networkStatsMgr = null; // ResourceStatsManager for network statistics.
+
+function errorCb(reason) {
+  ok(false, reason);
+}
+
+// Check the content returned by getAlarms.
+function checkAlarmsArray(alarms) {
+  // Check if data is an array.
+  if (!Array.isArray(alarms)) {
+    throw "getAlarms does not return an array.";
+  } else {
+    ok(true, "getAlarms returns an array.")
+  }
+
+  // Iterate the array and check the type of each element.
+  var obj = null;
+  var message = null; // Message for exception
+
+  for (var i = 0; i < alarms.length; i++) {
+    obj = alarms[i];
+
+    // Check if obj is an instance os ResourceStatsAlarm.
+    if (!(obj instanceof ResourceStatsAlarm)) {
+      message = "The array contains a non-ResourceStatsAlarm object.";
+      break;
+    }
+
+    // Check if obj.type is network.
+    if (obj.type != "network") {
+      message = "The type of a ResourceStatsAlarm object is not network.";
+      break;
+    }
+  }
+
+  if (message) {
+    throw message;
+  }
+
+  ok(true, "The return is an array of ResourceStatsAlarm objects.");
+}
+
+// Test Cases for testing WebIDL methods related to resource control.
+var testCases = [
+  function() {
+    // Test removeAllAlarms.
+    var promise = networkStatsMgr.removeAllAlarms();
+    promise.then(function() {
+      ok(true, "removeAllAlarms deleted all network alarms.");
+      testMethods();
+    }, function() {
+      ok(false, "removeAllAlarms failed to delete network alarms.");
+    });
+  },
+
+  function() {
+    // Test addAlarm.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': Date.now() });
+    promise.then(function(value) {
+      // Check the value (alarmId).
+      if (value < 0) {
+        ok(false, "addAlarm failed to create an alarm.");
+      } else {
+        ok(true, "addAlarm created an alarm.");
+        testMethods();
+      }
+    }, function() {
+      ok(false, "addAlarm failed to create an alarm.");
+    });
+  },
+
+  function() {
+    // Test addAlarm with negative threshold.
+    var threshold = -1;
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': Date.now() });
+    promise.then(function() {
+      // Check the value.
+      ok(false,
+         "addAlarm did not throw an exception when negative threshold is set.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when negative threshold is set.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test addAlarm with no threshold.
+    var promise = networkStatsMgr.addAlarm();
+    promise.then(function() {
+      // Check the value.
+      ok(false, "addAlarm did not throw an exception when no threshold.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when no threshold.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test addAlarm with negative startTime.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': -1 });
+    promise.then(function() {
+      // Check the value.
+      ok(false,
+         "addAlarm did not throw an exception when negative startTime is set.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when negative startTime is set.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test addAlarm when manifestURL is invalid.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent,
+                                             'manifestURL': invalidManifestURL },
+                                           { 'startTime': Date.now() });
+    promise.then(function() {
+      // Check the value.
+      ok(false, "addAlarm did not throw an exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when manifestURL is invalid.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test getAlarms.
+    var alarmId;
+    var alarms;
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': Date.now() });
+
+    // 2. Test getAlarms if new alarm is added.
+    var runGetAlarms = function(value) {
+      alarmId = value;
+      return networkStatsMgr.getAlarms({ 'component': wifiComponent });
+    };
+
+    // 3. Check the content returned by getAlarms.
+    var checkGetAlarmsReturn = function(value) {
+      alarms = value;
+      checkAlarmsArray(value);
+    };
+
+    // 4. Check the alarm added in step 1 is inside the return of getAlarms.
+    var checkAlarm = function (value) {
+      // Find the alarm.
+      var index = alarms.map(function(e) { return e.alarmId; })
+                    .indexOf(alarmId);
+      if (index < 0) {
+        throw "getAlarms does not get the alarm added in previous step.";
+      }
+      var alarm = alarms[index];
+
+      // Evaluate the alarm.
+      ok(alarm.threshold == threshold, "threshold is equal.");
+      ok(alarm.component == wifiComponent, "component is equal.");
+      ok(alarm.serviceType == null, "serviceType should be null.");
+      ok(alarm.manifestURL == null, "manifestURL should be null.");
+    };
+
+    // Create promise chaining.
+    promise.then(runGetAlarms)
+      .then(checkGetAlarmsReturn)
+      .then(checkAlarm)
+      .then(testMethods, errorCb); // Execute next test case.
+  },
+
+  function() {
+    // Test getAlarms with invalid manifestURL.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent,
+                                             'manifestURL': invalidManifestURL },
+                                           { 'startTime': Date.now() });
+
+    promise.then(function() {
+      // Check the value.
+      ok(false, "getAlarms did not throw an exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "getAlarms threw an exception when manifestURL is invalid.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test getAlarms with incorrect parameter.
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': Date.now() });
+
+    // 2. Call getAlarms with incorrect parameter.
+    var runGetAlarms = function() {
+      return networkStatsMgr.getAlarms({ 'component': mobileComponent });
+    };
+
+    // 3. check the content returned by getAlarms.
+    var checkGetAlarmsReturn = function(value) {
+      // Check array length
+      if (value.length) {
+        throw "getAlarms gets an alarm when using incorrect parameter.";
+      } else {
+        ok(true,
+           "getAlarms returns an empty array when using incorrect parameter.");
+      }
+    };
+
+    // Create pomise chaining.
+    promise.then(runGetAlarms)
+      .then(checkGetAlarmsReturn)
+      .then(testMethods, errorCb); // Execute next test case.
+  },
+
+  function() {
+    // Test removeAlarm
+    var alarmId;
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': Date.now() });
+
+    // 2. Try to remove the new alarm.
+    var runRemoveAlarm = function(value) {
+      alarmId = value;
+      return networkStatsMgr.removeAlarm(alarmId);
+    }
+
+    // Create promise chaining.
+    promise.then(runRemoveAlarm)
+      .then(function() {
+        ok(true, "removeAlarm deleted the alarm.");
+        testMethods();
+      }, errorCb);
+  },
+
+  function() {
+    // Test removeAlarm with negative alarmId
+    var alarmId = -1;
+    var promise = networkStatsMgr.removeAlarm(alarmId);
+    promise.then(function() {
+      ok(false,
+         "removeAlarm did not throw an exception when negative alarmId is set.");
+    }, function() {
+      ok(true, "removeAlarm threw an exception when negative alarmId is set.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test removeAlarm with invalid alarmId
+    var alarmId;
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = networkStatsMgr.addAlarm(threshold,
+                                           { 'component': wifiComponent },
+                                           { 'startTime': Date.now() });
+
+    // 2. Try to remove an invalid alarm.
+    var runRemoveAlarm = function(value) {
+      alarmId = value;
+      // Because alarmId is auto-increment, any alarmId larger than the
+      // latest alarmId should be invalid.
+      return networkStatsMgr.removeAlarm(alarmId + 10);
+    }
+
+    // Create promise chaining.
+    promise.then(runRemoveAlarm)
+      .then(function() {
+        // Input with incorrect alarmId should not be resolved.
+        throw "removeAlarm should fail with invalid alarmId.";
+      }, function(reason) {
+        if (reason == "alarm not existed") {
+          ok(true, "removeAlarm with invalid alarmId should fail.")
+        } else {
+          throw reason;
+        }
+      })
+      .then(testMethods, errorCb);
+  }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+  if (!testCases.length) {
+    ok(true, "Done.");
+    SpecialPowers.removePermission("resourcestats-manage", document);
+    SimpleTest.finish();
+    return;
+  }
+
+  var testCase = testCases.shift();
+  testCase();
+}
+
+function startTest() {
+  // Create an instance of ResourceStatsManager for network.
+  networkStatsMgr = new ResourceStatsManager("network");
+  ok(networkStatsMgr, "Create networkStatsMgr.");
+
+  // Test WebIDL methods related to resource control.
+  testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_network_stats.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for ResourceStats methods realted to network statistics</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+var networkStatsMgr = null; // ResourceStatsManager for network statistics.
+var sampleRate = 0;
+
+// Test WebIDL attributes.
+function testAttributes() {
+  // Test sampleRate.
+  ok('sampleRate' in networkStatsMgr,
+   "sampleRate should be a ResourceStatsManager attribute.");
+  sampleRate = networkStatsMgr.sampleRate;
+  ok(sampleRate > 0, "sampleRate is greater than 0.");
+
+  // Test maxStorageAge.
+  ok('maxStorageAge' in networkStatsMgr,
+   "maxStorageAge should be a ResourceStatsManager attribute.");
+  ok(networkStatsMgr.maxStorageAge > 0,
+   "maxStorageAge is greater than 0.");
+
+  // Test whether "network" in resourceTypes array.
+  ok('resourceTypes' in networkStatsMgr,
+   "resourceTypes should be a ResourceStatsManager attribute.");
+  ok(Array.isArray(networkStatsMgr.resourceTypes),
+   "networkStatsMgr.resourceTypes is an array.");
+  ok(networkStatsMgr.resourceTypes.indexOf("network") > -1,
+   "'network' is an element of networkStatsMgr.resourceTypes.");
+}
+
+// Check the content returned by ResourceStats.getData().
+function checkData(data, start, end) {
+  // Check if data is an array.
+  if (!Array.isArray(data)) {
+    ok(false, "getData does not return an array.")
+    return;
+  } else {
+    ok(true, "getData returns an array.")
+  }
+
+  // Iterate the array and check the timestamp and type of each element.
+  var success = true;
+  var obj = null;
+  var timestamp = start;
+  var i = 0;
+  var length = data.length;
+
+  do {
+    obj = data[i++];
+
+    // Check object type.
+    if (!(obj instanceof NetworkStatsData)) {
+      success = false;
+      ok(false, "The array contains a non-NetworkStatsData object.");
+      break;
+    }
+
+    // Check if the timestamp is continuous.
+    if (obj.timestamp !== timestamp) {
+      success = false;
+      ok(false, "The timestamp of NetworkStatsData object is correct.");
+      break;
+    }
+
+    timestamp += sampleRate;
+  } while (i < length);
+
+  if (!success) {
+    return;
+  }
+
+  // Check the timestamp of the last element is equal to end.
+  if (obj.timestamp != end) {
+    ok(false,
+       "The timestamp of the last element of the array is equal to end.");
+    return;
+  }
+
+  // Execute next test case.
+  ok(true, "The return of getData is an array of NetworkStatsData objects.");
+  testMethods();
+}
+
+// Test Cases for testing WebIDL methods.
+var testCases = [
+  function() {
+    // Test clearAllStats.
+    var promise = networkStatsMgr.clearAllStats();
+    promise.then(function() {
+      ok(true, "clearAllStats clears the network store.");
+      testMethods();
+    }, function() {
+      ok(false, "clearAllStats fails to clear the network store.");
+    });
+  },
+
+  function() {
+    // Test clearStats.
+    var promise = networkStatsMgr.clearStats();
+    promise.then(function() {
+      ok(true, "clearStats clears the network store.");
+      testMethods();
+    }, function() {
+      ok(false, "clearStats fails to clear the network store.");
+    });
+  },
+
+  function() {
+    // Check if clearStats throw exception when start is great than end.
+    var end = Date.now();
+    var start = end + 1000;
+    var promise = networkStatsMgr.clearStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "clearStats does not throw exception when start is great than end.");
+    }, function() {
+      ok(true, "clearStats throw exception when start is great than end.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if clearStats throw exception when start is less than 0.
+    var end = Date.now();
+    var start = -1;
+    var promise = networkStatsMgr.clearStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "clearStats dose not throw exception when start is less than 0.");
+    }, function() {
+      ok(true, "clearStats throw exception when start is less than 0.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if clearStats throw exception when manifestURL is invalid.
+    var options = {manifestURL: invalidManifestURL};
+    var promise = networkStatsMgr.clearStats(options);
+    promise.then(function() {
+      ok(false,
+         "clearStats does not throw exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "clearStats throw exception when manifestURL is invalid.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test getAvailableComponents.
+    var promise = networkStatsMgr.getAvailableComponents();
+    promise.then(function(value) {
+      if (Array.isArray(value)) {
+        ok(true, "getAvailableComponents returns an array.");
+        testMethods();
+      } else {
+        ok(false, "getAvailableComponents does not return an array.");
+      }
+    }, function() {
+      ok(false, "Fail to execute getAvailableComponents.");
+    });
+  },
+
+  function() {
+    // Test getStats.
+    ok(true, "Get system stats when start and end are adapted to sampleRate.");
+
+    // Prepare start and end.
+    var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    var end = Math.floor((Date.now() - offset) / sampleRate) * sampleRate + offset;
+    var start = end - sampleRate * 10;
+
+    // Launch request.
+    var promise = networkStatsMgr.getStats(null, start, end);
+    promise.then(function(value) {
+      // Check the object type.
+      if (value instanceof ResourceStats) {
+        ok(true, "Get a ResourceStats object.");
+      } else {
+        ok(false, "Fail to get a ResourceStats object.");
+        return;
+      }
+
+      // Check attributes of ResourceStats.
+      ok(value.type == "network", "type should be network.");
+      ok(value.component == null, "component should be null.");
+      ok(value.serviceType == null, "serviceType should be null.");
+      ok(value.manifestURL == null, "manifestURL should be null.");
+
+      // Check if the time range of ResourceStats is equal to the request.
+      ok(value.start == start, "start timestamp should be equal.");
+      ok(value.end == end, "end timestamp should be equal.");
+
+      // Check stats stored inside ResourceStats.
+      if ('getData' in value) {
+        checkData(value.getData(), start, end);
+      } else {
+        ok(false, "getData is not a method of ResourceStats.");
+        return;
+      }
+    }, function() {
+      ok(false, "Get network stats failed.");
+    });
+  },
+
+  function() {
+    // Test getStats when start and end are not adapted to sampleRate.
+    ok(true,
+       "Get system stats when start and end are not adapted to sampleRate.");
+
+    // Prepare start and end.
+    var end = Date.now();
+    var start = end - sampleRate * 10;
+
+    // Normalize start and end.
+    var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    var normEnd = Math.floor((end - offset) / sampleRate)
+                    * sampleRate + offset;
+    var normStart = Math.floor((start - offset) / sampleRate)
+                      * sampleRate + offset;
+
+    // Launch request.
+    var promise = networkStatsMgr.getStats(null, start, end);
+    promise.then(function(value) {
+      // Check the object type.
+      if (value instanceof ResourceStats) {
+        ok(true, "Get a ResourceStats object.");
+      } else {
+        ok(false, "Fail to get a ResourceStats object.");
+        return;
+      }
+
+      // Check attributes of ResourceStats.
+      ok(value.type == "network", "type should be network.");
+      ok(value.component == null, "component should be null.");
+      ok(value.serviceType == null, "serviceType should be null.");
+      ok(value.manifestURL == null, "manifestURL should be null.");
+
+      // Check if time range of ResourceStats are normalized.
+      ok(value.start == normStart, "start timestamp should be normalized.");
+      ok(value.end == normEnd, "end timestamp should be normalized.");
+
+      // Check stats stored inside ResourceStats.
+      if ('getData' in value) {
+        checkData(value.getData(), normStart, normEnd);
+      } else {
+        ok(false, "getData is not a method of ResourceStats.");
+        return;
+      }
+    }, function() {
+      ok(false, "Get network stats failed.");
+    });
+  },
+
+  function () {
+    // Check if getStats throw exception when start is greater than end.
+    var end = Date.now();
+    var start = end + 1000;
+    var promise = networkStatsMgr.getStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "getStats dose not throw exception when start is great than end.");
+    }, function() {
+      ok(true, "getStats throw exception when start is great than end.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if getStats throw exception when start is less than 0.
+    var end = Date.now();
+    var start = -1;
+    var promise = networkStatsMgr.getStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "getStats dose not throw exception when start is less than 0.");
+    }, function() {
+      ok(true, "getStats throw exception when start is less than 0.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if getStats throw exception when manifestURL is invalid.
+    var options = {manifestURL: invalidManifestURL};
+    var promise = networkStatsMgr.getStats(options);
+    promise.then(function(value) {
+      ok(false,
+         "getStats does not throw exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "getStats throw exception when manifestURL is invalid.");
+      testMethods();
+    });
+  }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+  if (!testCases.length) {
+    ok(true, "Done.");
+    SpecialPowers.removePermission("resourcestats-manage", document);
+    SimpleTest.finish();
+    return;
+  }
+
+  var testCase = testCases.shift();
+  testCase();
+}
+
+function startTest() {
+  // Create an instance of ResourceStatsManager for network stats.
+  networkStatsMgr = new window.ResourceStatsManager("network");
+  ok(networkStatsMgr, "Create networkStatsMgr.");
+
+  // Test WebIDL attributes.
+  testAttributes();
+
+  // Test WebIDL methods related to stats operation.
+  testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_no_perm.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test to ensure ResourceStatsManager is not accessible without permission</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to ensure ResourceStatsManager is not accessible without permission.
+SpecialPowers.removePermission("resourcestats-manage", document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, function() {
+  ok(!(SpecialPowers.hasPermission("resourcestats-manage", document)),
+     "Do not have permission 'resourcestats-manage'.");
+  ok(SpecialPowers.getBoolPref("dom.resource_stats.enabled"),
+     "Preference 'dom.resource_stats.enabled' is true.");
+
+  // Check ResourceStatsManager exist.
+  ok(!('ResourceStatsManager' in window),
+     "Cannot access window.ResourceStatsManager when have no permission");
+
+  SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_not_supported_type.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test to ensure ResourceStatsManager does not create an instance for non-supported type</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const type = "non-supported";
+
+SimpleTest.waitForExplicitFinish();
+
+// Test to ensure ResourceStatsManager does not create an instance for
+// non-supported type.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, function() {
+  try {
+    var mgr = ResourceStatsManager(type);
+    ok(false,
+       "Creating an instance for non-supported type should throw an exeception.");
+  } catch (ex) {
+    ok(ex,
+       "Got an exception when creating an instance for non-supported type.");
+  }
+
+  SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_power_alarm.html
@@ -0,0 +1,355 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for ResourceStats methods realted to power resource control</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+const cpuComponent = "cpu:0";
+const gpsComponent = "gps:0";
+var powerStatsMgr = null; // ResourceStatsManager for power statistics.
+
+function errorCb(reason) {
+  ok(false, reason);
+}
+
+// Check the content returned by getAlarms.
+function checkAlarmsArray(alarms) {
+  // Check if data is an array.
+  if (!Array.isArray(alarms)) {
+    throw "getAlarms does not return an array.";
+  } else {
+    ok(true, "getAlarms returns an array.")
+  }
+
+  // Iterate the array and check the type of each element.
+  var obj = null;
+  var message = null; // Message for exception
+
+  for (var i = 0; i < alarms.length; i++) {
+    obj = alarms[i];
+
+    // Check if obj is an instance os ResourceStatsAlarm.
+    if (!(obj instanceof ResourceStatsAlarm)) {
+      message = "The array contains a non-ResourceStatsAlarm object.";
+      break;
+    }
+
+    // Check if obj.type is power.
+    if (obj.type != "power") {
+      message = "The type of a ResourceStatsAlarm object is not power.";
+      break;
+    }
+  }
+
+  if (message) {
+    throw message;
+  }
+
+  ok(true, "The return is an array of ResourceStatsAlarm objects.");
+}
+
+// Test Cases for testing WebIDL methods related to resource control.
+var testCases = [
+  function() {
+    // Test removeAllAlarms.
+    var promise = powerStatsMgr.removeAllAlarms();
+    promise.then(function() {
+      ok(true, "removeAllAlarms deleted all power alarms.");
+      testMethods();
+    }, function() {
+      ok(false, "removeAllAlarms failed to delete power alarms.");
+    });
+  },
+
+  function() {
+    // Test addAlarm.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': Date.now() });
+    promise.then(function(value) {
+      // Check the value (alarmId).
+      if (value < 0) {
+        ok(false, "addAlarm failed to create an alarm.");
+      } else {
+        ok(true, "addAlarm created an alarm.");
+        testMethods();
+      }
+    }, function() {
+      ok(false, "addAlarm failed to create an alarm.");
+    });
+  },
+
+  function() {
+    // Test addAlarm with negative threshold.
+    var threshold = -1;
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': Date.now() });
+    promise.then(function() {
+      // Check the value.
+      ok(false,
+         "addAlarm did not throw an exception when negative threshold is set.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when negative threshold is set.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test addAlarm with no threshold.
+    var promise = powerStatsMgr.addAlarm();
+    promise.then(function() {
+      // Check the value.
+      ok(false, "addAlarm did not throw an exception when no threshold.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when no threshold.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test addAlarm with negative startTime.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': -1 });
+    promise.then(function() {
+      // Check the value.
+      ok(false,
+         "addAlarm did not throw an exception when negative startTime is set.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when negative startTime is set.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test addAlarm when manifestURL is invalid.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent,
+                                           'manifestURL': invalidManifestURL },
+                                         { 'startTime': Date.now() });
+    promise.then(function() {
+      // Check the value.
+      ok(false, "addAlarm did not throw an exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "addAlarm threw an exception when manifestURL is invalid.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test getAlarms.
+    var alarmId;
+    var alarms;
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': Date.now() });
+
+    // 2. Test getAlarms if new alarm is added.
+    var runGetAlarms = function(value) {
+      alarmId = value;
+      return powerStatsMgr.getAlarms({ 'component': cpuComponent });
+    };
+
+    // 3. Check the content returned by getAlarms.
+    var checkGetAlarmsReturn = function(value) {
+      alarms = value;
+      checkAlarmsArray(value);
+    };
+
+    // 4. Check the alarm added in step 1 is inside the return of getAlarms.
+    var checkAlarm = function (value) {
+      // Find the alarm.
+      var index = alarms.map(function(e) { return e.alarmId; })
+                    .indexOf(alarmId);
+      if (index < 0) {
+        throw "getAlarms does not get the alarm added in previous step.";
+      }
+      var alarm = alarms[index];
+
+      // Evaluate the alarm.
+      ok(alarm.threshold == threshold, "threshold is equal.");
+      ok(alarm.component == cpuComponent, "component is equal.");
+      ok(alarm.serviceType == null, "serviceType should be null.");
+      ok(alarm.manifestURL == null, "manifestURL should be null.");
+    };
+
+    // Create promise chaining.
+    promise.then(runGetAlarms)
+      .then(checkGetAlarmsReturn)
+      .then(checkAlarm)
+      .then(testMethods, errorCb); // Execute next test case.
+  },
+
+  function() {
+    // Test getAlarms with invalid manifestURL.
+    var threshold = Math.floor(Math.random() * 1000);
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent,
+                                           'manifestURL': invalidManifestURL },
+                                         { 'startTime': Date.now() });
+
+    promise.then(function() {
+      // Check the value.
+      ok(false, "getAlarms did not throw an exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "getAlarms threw an exception when manifestURL is invalid.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test getAlarms with incorrect parameter.
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': Date.now() });
+
+    // 2. Call getAlarms with incorrect parameter.
+    var runGetAlarms = function() {
+      return powerStatsMgr.getAlarms({ 'component': gpsComponent });
+    };
+
+    // 3. check the content returned by getAlarms.
+    var checkGetAlarmsReturn = function(value) {
+      // Check array length
+      if (value.length) {
+        throw "getAlarms gets an alarm when using incorrect parameter.";
+      } else {
+        ok(true,
+           "getAlarms returns an empty array when using incorrect parameter.");
+      }
+    };
+
+    // Create pomise chaining.
+    promise.then(runGetAlarms)
+      .then(checkGetAlarmsReturn)
+      .then(testMethods, errorCb); // Execute next test case.
+  },
+
+  function() {
+    // Test removeAlarm
+    var alarmId;
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': Date.now() });
+
+    // 2. Try to remove the new alarm.
+    var runRemoveAlarm = function(value) {
+      alarmId = value;
+      return powerStatsMgr.removeAlarm(alarmId);
+    }
+
+    // Create promise chaining.
+    promise.then(runRemoveAlarm)
+      .then(function() {
+        ok(true, "removeAlarm deleted the alarm.");
+        testMethods();
+      }, errorCb);
+  },
+
+  function() {
+    // Test removeAlarm with negative alarmId
+    var alarmId = -1;
+    var promise = powerStatsMgr.removeAlarm(alarmId);
+    promise.then(function() {
+      ok(false,
+         "removeAlarm did not throw an exception when negative alarmId is set.");
+    }, function() {
+      ok(true, "removeAlarm threw an exception when negative alarmId is set.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test removeAlarm with invalid alarmId
+    var alarmId;
+    var threshold = Math.floor(Math.random() * 1000);
+
+    // Execution steps:
+    // 1. Add a new alarm.
+    var promise = powerStatsMgr.addAlarm(threshold,
+                                         { 'component': cpuComponent },
+                                         { 'startTime': Date.now() });
+
+    // 2. Try to remove an invalid alarm.
+    var runRemoveAlarm = function(value) {
+      alarmId = value;
+      // Because alarmId is auto-increment, any alarmId larger than the
+      // latest alarmId should be invalid.
+      return powerStatsMgr.removeAlarm(alarmId + 10);
+    }
+
+    // Create promise chaining.
+    promise.then(runRemoveAlarm)
+      .then(function() {
+        // Input with incorrect alarmId should not be resolved.
+        throw "removeAlarm should fail with invalid alarmId.";
+      }, function(reason) {
+        if (reason == "alarm not existed") {
+          ok(true, "removeAlarm with invalid alarmId should fail.")
+        } else {
+          throw reason;
+        }
+      })
+      .then(testMethods, errorCb);
+  }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+  if (!testCases.length) {
+    ok(true, "Done.");
+    SpecialPowers.removePermission("resourcestats-manage", document);
+    SimpleTest.finish();
+    return;
+  }
+
+  var testCase = testCases.shift();
+  testCase();
+}
+
+function startTest() {
+  // Create an instance of ResourceStatsManager for power.
+  powerStatsMgr = new ResourceStatsManager("power");
+  ok(powerStatsMgr, "Create powerStatsMgr.");
+
+  // Test WebIDL methods related to resource control.
+  testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, startTest);
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/mochitest/test_power_stats.html
@@ -0,0 +1,345 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for ResourceStats methods realted to power statistics</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp";
+var powerStatsMgr = null; // ResourceStatsManager for power statistics.
+var sampleRate = 0;
+
+// Test WebIDL attributes.
+function testAttributes() {
+  // Test sampleRate.
+  ok('sampleRate' in powerStatsMgr,
+   "sampleRate should be a ResourceStatsManager attribute.");
+  sampleRate = powerStatsMgr.sampleRate;
+  ok(sampleRate > 0, "sampleRate is greater than 0.");
+
+  // Test maxStorageAge.
+  ok('maxStorageAge' in powerStatsMgr,
+   "maxStorageAge should be a ResourceStatsManager attribute.");
+  ok(powerStatsMgr.maxStorageAge > 0,
+   "maxStorageAge is greater than 0.");
+
+  // Test whether "power" in resourceTypes array.
+  ok('resourceTypes' in powerStatsMgr,
+   "resourceTypes should be a ResourceStatsManager attribute.");
+  ok(Array.isArray(powerStatsMgr.resourceTypes),
+   "powerStatsMgr.resourceTypes is an array.");
+  ok(powerStatsMgr.resourceTypes.indexOf("power") > -1,
+   "'power' is an element of powerStatsMgr.resourceTypes.");
+}
+
+// Check the content returned by ResourceStats.getData().
+function checkData(data, start, end) {
+  // Check if data is an array.
+  if (!Array.isArray(data)) {
+    ok(false, "getData does not return an array.")
+    return;
+  } else {
+    ok(true, "getData returns an array.")
+  }
+
+  // Iterate the array and check the timestamp and type of each element.
+  var success = true;
+  var obj = null;
+  var timestamp = start;
+  var i = 0;
+  var length = data.length;
+
+  do {
+    obj = data[i++];
+
+    // Check object type.
+    if (!(obj instanceof PowerStatsData)) {
+      success = false;
+      ok(false, "The array contains a non-PowerStatsData object.");
+      break;
+    }
+
+    // Check if the timestamp is continuous.
+    if (obj.timestamp != timestamp) {
+      success = false;
+      ok(false, "The timestamp of PowerStatsData object is correct.");
+      break;
+    }
+
+    timestamp += sampleRate;
+  } while (i < length);
+
+  if (!success) {
+    return;
+  }
+
+  // Check the timestamp of the last element is equal to end.
+  if (obj.timestamp != end) {
+    ok(false,
+       "The timestamp of the last element of the array is equal to end.");
+    return;
+  }
+
+  // Execute next test case.
+  ok(true, "The return of getData is an array of PowerStatsData objects.");
+  testMethods();
+}
+
+// Test Cases for testing WebIDL methods.
+var testCases = [
+  function() {
+    // Test clearAllStats.
+    var promise = powerStatsMgr.clearAllStats();
+    promise.then(function() {
+      ok(true, "clearAllStats clears the power store.");
+      testMethods();
+    }, function() {
+      ok(false, "clearAllStats fails to clear the power store.");
+    });
+  },
+
+  function() {
+    // Test clearStats.
+    var promise = powerStatsMgr.clearStats();
+    promise.then(function() {
+      ok(true, "clearStats clears the power store.");
+      testMethods();
+    }, function() {
+      ok(false, "clearStats fails to clear the power store.");
+    });
+  },
+
+  function() {
+    // Check if clearStats throw exception when start is great than end.
+    var end = Date.now();
+    var start = end + 1000;
+    var promise = powerStatsMgr.clearStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "clearStats does not throw exception when start is great than end.");
+    }, function() {
+      ok(true, "clearStats throw exception when start is great than end.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if clearStats throw exception when start is less than 0.
+    var end = Date.now();
+    var start = -1;
+    var promise = powerStatsMgr.clearStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "clearStats dose not throw exception when start is less than 0.");
+    }, function() {
+      ok(true, "clearStats throw exception when start is less than 0.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if clearStats throw exception when manifestURL is invalid.
+    var options = {manifestURL: invalidManifestURL};
+    var promise = powerStatsMgr.clearStats(options);
+    promise.then(function() {
+      ok(false,
+         "clearStats does not throw exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "clearStats throw exception when manifestURL is invalid.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Test getAvailableComponents.
+    var promise = powerStatsMgr.getAvailableComponents();
+    promise.then(function(value) {
+      if (Array.isArray(value)) {
+        ok(true, "getAvailableComponents returns an array.");
+        testMethods();
+      } else {
+        ok(false, "getAvailableComponents does not return an array.");
+      }
+    }, function() {
+      ok(false, "Fail to execute getAvailableComponents.");
+    });
+  },
+
+  function() {
+    // Test getStats.
+    ok(true, "Get system stats when start and end are adapted to sampleRate.");
+
+    // Prepare start and end.
+    var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    var end = Math.floor((Date.now() - offset) / sampleRate) * sampleRate + offset;
+    var start = end - sampleRate * 10;
+
+    // Launch request.
+    var promise = powerStatsMgr.getStats(null, start, end);
+    promise.then(function(value) {
+      // Check the object type.
+      if (value instanceof ResourceStats) {
+        ok(true, "Get a ResourceStats object.");
+      } else {
+        ok(false, "Fail to get a ResourceStats object.");
+        return;
+      }
+
+      // Check attributes of ResourceStats.
+      ok(value.type == "power", "type should be power.");
+      ok(value.component == null, "component should be null.");
+      ok(value.serviceType == null, "serviceType should be null.");
+      ok(value.manifestURL == null, "manifestURL should be null.");
+
+      // Check if the time range of ResourceStats is equal to the request.
+      ok(value.start == start, "start timestamp should be equal.");
+      ok(value.end == end, "end timestamp should be equal.");
+
+      // Check stats stored inside ResourceStats.
+      if ('getData' in value) {
+        checkData(value.getData(), start, end);
+      } else {
+        ok(false, "getData is not a method of ResourceStats.");
+        return;
+      }
+    }, function() {
+      ok(false, "Get power stats failed.");
+    });
+  },
+
+  function() {
+    // Test getStats when start and end are not adapted to sampleRate.
+    ok(true,
+       "Get system stats when start and end are not adapted to sampleRate.");
+
+    // Prepare start and end.
+    var end = Date.now();
+    var start = end - sampleRate * 10;
+
+    // Normalize start and end.
+    var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    var normEnd = Math.floor((end - offset) / sampleRate)
+                    * sampleRate + offset;
+    var normStart = Math.floor((start - offset) / sampleRate)
+                      * sampleRate + offset;
+
+    // Launch request.
+    var promise = powerStatsMgr.getStats(null, start, end);
+    promise.then(function(value) {
+      // Check the object type.
+      if (value instanceof ResourceStats) {
+        ok(true, "Get a ResourceStats object.");
+      } else {
+        ok(false, "Fail to get a ResourceStats object.");
+        return;
+      }
+
+      // Check attributes of ResourceStats.
+      ok(value.type == "power", "type should be power.");
+      ok(value.component == null, "component should be null.");
+      ok(value.serviceType == null, "serviceType should be null.");
+      ok(value.manifestURL == null, "manifestURL should be null.");
+
+      // Check if time range of ResourceStats are normalized.
+      ok(value.start == normStart, "start timestamp should be normalized.");
+      ok(value.end == normEnd, "end timestamp should be normalized.");
+
+      // Check stats stored inside ResourceStats.
+      if ('getData' in value) {
+        checkData(value.getData(), normStart, normEnd);
+      } else {
+        ok(false, "getData is not a method of ResourceStats.");
+        return;
+      }
+    }, function() {
+      ok(false, "Get power stats failed.");
+    });
+  },
+
+  function() {
+    // Check if getStats throw exception when start is greater than end.
+    var end = Date.now();
+    var start = end + 1000;
+    var promise = powerStatsMgr.getStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "getStats dose not throw exception when start is great than end.");
+    }, function() {
+      ok(true, "getStats throw exception when start is great than end.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if getStats throw exception when start is less than 0.
+    var end = Date.now();
+    var start = -1;
+    var promise = powerStatsMgr.getStats(null, start, end);
+    promise.then(function() {
+      ok(false,
+         "getStats dose not throw exception when start is less than 0.");
+    }, function() {
+      ok(true, "getStats throw exception when start is less than 0.");
+      testMethods();
+    });
+  },
+
+  function() {
+    // Check if getStats throw exception when manifestURL is invalid.
+    var options = {manifestURL: invalidManifestURL};
+    var promise = powerStatsMgr.getStats(options);
+    promise.then(function(value) {
+      ok(false,
+         "getStats does not throw exception when manifestURL is invalid.");
+    }, function() {
+      ok(true, "getStats throw exception when manifestURL is invalid.");
+      testMethods();
+    });
+  }
+];
+
+// Test WebIDL methods related stats operation.
+function testMethods() {
+  if (!testCases.length) {
+    ok(true, "Done.");
+    SpecialPowers.removePermission("resourcestats-manage", document);
+    SimpleTest.finish();
+    return;
+  }
+
+  var testCase = testCases.shift();
+  testCase();
+}
+
+function startTest() {
+  // Create an instance of ResourceStatsManager for power stats.
+  powerStatsMgr = new ResourceStatsManager("power");
+  ok(powerStatsMgr, "Create powerStatsMgr.");
+
+  // Test WebIDL attributes.
+  testAttributes();
+
+  // Test WebIDL methods related to stats operation.
+  testMethods();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// Enable permission and preference.
+SpecialPowers.addPermission("resourcestats-manage", true, document);
+SpecialPowers.pushPrefEnv({ 'set': [
+                            ["dom.resource_stats.enabled", true],
+                            ["dom.ignore_webidl_scope_checks", true]
+                          ]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/moz.build
@@ -0,0 +1,10 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG['MOZ_B2G_RIL']:
+    XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini']
+
+MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini']
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js
@@ -0,0 +1,985 @@
+/* Any: copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ResourceStatsDB.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const db = new ResourceStatsDB();
+
+// Components.
+const wifiComponent = "wifi:0";
+const mobileComponent = "mobile:1";
+const cpuComponent = "cpu:0";
+const gpsComponent = "gps:0";
+
+// List of available components.
+const networkComponents = [wifiComponent, mobileComponent];
+const powerComponents = [cpuComponent, gpsComponent];
+const offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+// Clear store.
+function clearStore(store, callback) {
+  db._dbNewTxn(store, "readwrite", function(aTxn, aStore) {
+    aStore.openCursor().onsuccess = function (event) {
+      let cursor = event.target.result;
+      if (cursor){
+        cursor.delete();
+        cursor.continue();
+      }
+    };
+  }, callback);
+}
+
+// Clear all stores to avoid starting tests with unknown state.
+add_test(function prepareDatabase() {
+  clearStore('power_stats_store', function() {
+    clearStore('network_stats_store', function() {
+      clearStore('alarm_store', function() {
+        run_next_test();
+      });
+    });
+  });
+});
+
+// Dump data saved in a store.
+function dumpStore(store, callback) {
+  db._dbNewTxn(store, "readonly", function(aTxn, aStore) {
+    aStore.mozGetAll().onsuccess = function onsuccess(event) {
+      aTxn.result = event.target.result;
+    };
+  }, callback);
+}
+
+// Check sampleRate is unchangeable.
+add_test(function test_sampleRate() {
+  var sampleRate = db.sampleRate;
+  do_check_true(sampleRate > 0);
+
+  db.sampleRate = 0;
+  sampleRate = db.sampleRate;
+  do_check_true(sampleRate > 0);
+
+  run_next_test();
+});
+
+// Test maxStorageAge is unchangeable.
+add_test(function test_maxStorageAge() {
+  var maxStorageAge = db.maxStorageAge;
+  do_check_true(maxStorageAge > 0);
+
+  db.maxStorageAge = 0;
+  maxStorageAge = db.maxStorageAge;
+  do_check_true(maxStorageAge > 0);
+
+  run_next_test();
+});
+
+// Normalize timestamp to sampleRate precision.
+function normalizeTime(aTimeStamp) {
+  var sampleRate = db.sampleRate;
+
+  return Math.floor((aTimeStamp - offset) / sampleRate) * sampleRate;
+}
+
+// Generte record as input for saveNetworkStats() as well as the expected
+// result when calling getStats().
+function generateNetworkRecord(aAppId, aServiceType, aComponents) {
+  var result = [];
+  var componentStats = {};
+  var receivedBytes;
+  var sentBytes;
+  var totalReceivedBytes = 0;
+  var totalSentBytes = 0;
+
+  aComponents.forEach(function(comp) {
+    // Step 1: generate random value for receivedBytes and sentBytes.
+    receivedBytes = Math.floor(Math.random() * 1000);
+    sentBytes = Math.floor(Math.random() * 1000);
+    totalReceivedBytes += receivedBytes;
+    totalSentBytes += sentBytes;
+
+    // Step 2: add stats to record.componentStats.
+    componentStats[comp] = {
+      receivedBytes: receivedBytes,
+      sentBytes: sentBytes
+    };
+
+    // Step 3: generate expected results.
+    result.push({
+      appId: aAppId,
+      serviceType: aServiceType,
+      component: comp,
+      receivedBytes: receivedBytes,
+      sentBytes: sentBytes
+    });
+  });
+
+  // Step 4: generate expected total stats.
+  result.push({
+    appId: aAppId,
+    serviceType: aServiceType,
+    component: "",
+    receivedBytes: totalReceivedBytes,
+    sentBytes: totalSentBytes
+  });
+
+  // Step 5: get record.
+  var record = { appId: aAppId,
+                 serviceType: aServiceType,
+                 componentStats: componentStats };
+
+  return { record: record, result: result };
+}
+
+// Generte record as input for savePowerStats() as well as the expected
+// result when calling getStats().
+function generatePowerRecord(aAppId, aServiceType, aComponents) {
+  var result = [];
+  var componentStats = {};
+  var consumedPower;
+  var totalConsumedPower = 0;
+
+  aComponents.forEach(function(comp) {
+    // Step 1: generate random value for consumedPower.
+    consumedPower = Math.floor(Math.random() * 1000);
+    totalConsumedPower += consumedPower;
+
+    // Step 2: add stats to record.componentStats.
+    componentStats[comp] = consumedPower;
+
+    // Step 3: generate expected results.
+    result.push({
+      appId: aAppId,
+      serviceType: aServiceType,
+      component: comp,
+      consumedPower: consumedPower
+    });
+  });
+
+  // Step 4: generate expected total stats.
+  result.push({
+    appId: aAppId,
+    serviceType: aServiceType,
+    component: "",
+    consumedPower: totalConsumedPower
+  });
+
+  // Step 5: get record.
+  var record = { appId: aAppId,
+                 serviceType: aServiceType,
+                 componentStats: componentStats };
+
+  return { record: record, result: result };
+}
+
+// Compare stats saved in network store with expected results.
+function checkNetworkStatsStore(aExpectedResult, aDumpResult, aTimestamp) {
+  // Step 1: a quick check for the length of arrays first.
+  do_check_eq(aExpectedResult.length, aDumpResult.length);
+
+  // Step 2: create a map array for search by receivedBytes.
+  var mapArray = aExpectedResult.map(function(e) {return e.receivedBytes;});
+
+  // Step 3: compare each element to make sure both array are equal.
+  var index;
+  var target;
+
+  aDumpResult.forEach(function(stats) {
+    index = 0;
+    // Find the first equal receivedBytes since index.
+    while ((index = mapArray.indexOf(stats.receivedBytes, index)) > -1) {
+      // Compare all attributes.
+      target = aExpectedResult[index];
+      if (target.appId != stats.appId ||
+          target.serviceType != stats.serviceType ||
+          target.component != stats.component ||
+          target.sentBytes != stats.sentBytes ||
+          aTimestamp != stats.timestamp) {
+        index += 1;
+        continue;
+      } else {
+        // If found, remove that element from aExpectedResult and mapArray.
+        aExpectedResult.splice(index, 1);
+        mapArray.splice(index, 1);
+        break;
+      }
+    }
+    do_check_neq(index, -1);
+  });
+  run_next_test();
+}
+
+// Compare stats saved in power store with expected results.
+function checkPowerStatsStore(aExpectedResult, aDumpResult, aTimestamp) {
+  // Step 1: a quick check for the length of arrays first.
+  do_check_eq(aExpectedResult.length, aDumpResult.length);
+
+  // Step 2: create a map array for search by consumedPower.
+  var mapArray = aExpectedResult.map(function(e) {return e.consumedPower;});
+
+  // Step 3: compare each element to make sure both array are equal.
+  var index;
+  var target;
+
+  aDumpResult.forEach(function(stats) {
+    index = 0;
+    // Find the first equal consumedPower since index.
+    while ((index = mapArray.indexOf(stats.consumedPower, index)) > -1) {
+      // Compare all attributes.
+      target = aExpectedResult[index];
+      if (target.appId != stats.appId ||
+          target.serviceType != stats.serviceType ||
+          target.component != stats.component ||
+          aTimestamp != stats.timestamp) {
+        index += 1;
+        continue;
+      } else {
+        // If found, remove that element from aExpectedResult and mapArray.
+        aExpectedResult.splice(index, 1);
+        mapArray.splice(index, 1);
+        break;
+      }
+    }
+    do_check_neq(index, -1);
+  });
+  run_next_test();
+}
+
+// Prepare network store for testing.
+function prepareNetworkStatsStore(recordArray, timestamp, callback) {
+  // Step 1: clear store.
+  clearStore("network_stats_store", function() {
+    // Step 2: save record to store.
+    db.saveNetworkStats(recordArray, timestamp, callback);
+  });
+}
+
+// Prepare power store for testing.
+function preparePowerStatsStore(recordArray, timestamp, callback) {
+  // Step 1: clear store.
+  clearStore("power_stats_store", function() {
+    // Step 2: save record to store.
+    db.savePowerStats(recordArray, timestamp, callback);
+  });
+}
+
+// Test saveNetworkStats.
+add_test(function test_saveNetworkStats() {
+  var appId = 1;
+  var serviceType = "";
+
+  // Step 1: generate data saved to store.
+  var { record: record, result: expectedResult } =
+    generateNetworkRecord(appId, serviceType, networkComponents);
+  var recordArray = [record];
+  var timestamp = Date.now();
+
+  // Step 2: save recordArray to network store.
+  prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: check if the function call succeed.
+    do_check_eq(error, null);
+    // Step 4: dump store for comparison.
+    dumpStore("network_stats_store", function(error, result) {
+      do_check_eq(error, null);
+      checkNetworkStatsStore(expectedResult, result, normalizeTime(timestamp));
+    });
+  });
+});
+
+// Test savePowerStats.
+add_test(function test_savePowerStats() {
+  var appId = 1;
+  var serviceType = "";
+
+  // Step 1: generate data saved to store.
+  var { record: record, result: expectedResult } =
+    generatePowerRecord(appId, serviceType, powerComponents);
+  var recordArray = [record];
+  var timestamp = Date.now();
+
+  // Step 2: save recordArray to power store.
+  preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: check if the function call succeed.
+    do_check_eq(error, null);
+    // Step 4: dump store for comparison.
+    dumpStore("power_stats_store", function(error, result) {
+      do_check_eq(error, null);
+      checkPowerStatsStore(expectedResult, result, normalizeTime(timestamp));
+    });
+  });
+});
+
+// Test getting network stats via getStats.
+add_test(function test_getNetworkStats() {
+  var appId = 0;
+  var manifestURL = "";
+  var serviceType = "";
+  var component = "";
+
+  // Step 1: generate data saved to store.
+  var { record: record, result: result } =
+    generateNetworkRecord(appId, serviceType, networkComponents);
+  var recordArray = [record];
+  var expectedStats = result[result.length - 1]; // Check total stats only.
+  var timestamp = Date.now();
+  var end = normalizeTime(timestamp) + offset;
+  var start = end;
+
+  // Step 2: save record and prepare network store.
+  prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: get network stats.
+    db.getStats("network", manifestURL, serviceType, component, start, end,
+      function(error, result) {
+      do_check_eq(error, null);
+
+      // Step 4: verify result.
+      do_check_eq(result.type, "network");
+      do_check_eq(result.manifestURL, manifestURL);
+      do_check_eq(result.serviceType, serviceType);
+      do_check_eq(result.component, component);
+      do_check_eq(result.start, start);
+      do_check_eq(result.end, end);
+      do_check_eq(result.sampleRate, db.sampleRate);
+      do_check_true(Array.isArray(result.statsData));
+      do_check_eq(result.statsData.length, 1);
+      var stats = result.statsData[0];
+      do_check_eq(stats.receivedBytes, expectedStats.receivedBytes);
+      do_check_eq(stats.sentBytes, expectedStats.sentBytes);
+
+      run_next_test(); // If success, run next test.
+    });
+  });
+});
+
+// Test getting power stats via getStats.
+add_test(function test_getPowerStats() {
+  var appId = 0;
+  var manifestURL = "";
+  var serviceType = "";
+  var component = "";
+
+  // Step 1: generate data saved to store.
+  var { record: record, result: result } =
+    generatePowerRecord(appId, serviceType, powerComponents);
+  var recordArray = [record];
+  var expectedStats = result[result.length - 1]; // Check total stats only.
+  var timestamp = Date.now();
+  var end = normalizeTime(timestamp) + offset;
+  var start = end;
+
+  // Step 2: save record and prepare power store.
+  preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: get power stats.
+    db.getStats("power", manifestURL, serviceType, component, start, end,
+      function(error, result) {
+      do_check_eq(error, null);
+
+      // Step 4: verify result
+      do_check_eq(result.type, "power");
+      do_check_eq(result.manifestURL, manifestURL);
+      do_check_eq(result.serviceType, serviceType);
+      do_check_eq(result.component, component);
+      do_check_eq(result.start, start);
+      do_check_eq(result.end, end);
+      do_check_eq(result.sampleRate, db.sampleRate);
+      do_check_true(Array.isArray(result.statsData));
+      do_check_eq(result.statsData.length, 1);
+      var stats = result.statsData[0];
+      do_check_eq(stats.consumedPower, expectedStats.consumedPower);
+
+      run_next_test(); // If success, run next test.
+    });
+  });
+});
+
+// Test deleting network stats via clearStats.
+add_test(function test_clearNetworkStats() {
+  var appId = 0;
+  var manifestURL = "";
+  var serviceType = "";
+  var component = "";
+
+  // Step 1: genrate data saved to network store.
+  var { record: record, result: result } =
+    generateNetworkRecord(appId, serviceType, networkComponents);
+  var recordArray = [record];
+  var timestamp = Date.now();
+  var end = normalizeTime(timestamp) + offset;
+  var start = end;
+
+  // Step 2: save record and prepare network store.
+  prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: clear network stats.
+    db.clearStats("network", appId, serviceType, component, start, end,
+      function(error, result) {
+      do_check_eq(error, null);
+
+      // Step 4: check if the stats is deleted.
+      db.getStats("network", manifestURL, serviceType, component, start, end,
+        function(error, result) {
+        do_check_eq(result.statsData.length, 0);
+        run_next_test();
+      });
+    });
+  });
+});
+
+// Test deleting power stats via clearStats.
+add_test(function test_clearPowerStats() {
+  var appId = 0;
+  var manifestURL = "";
+  var serviceType = "";
+  var component = "";
+
+  // Step 1: genrate data saved to power store.
+  var { record: record, result: result } =
+    generatePowerRecord(appId, serviceType, powerComponents);
+  var recordArray = [record];
+  var timestamp = Date.now();
+  var end = normalizeTime(timestamp) + offset;
+  var start = end;
+
+  // Step 2: save record and prepare power store.
+  preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: clear power stats.
+    db.clearStats("power", appId, serviceType, component, start, end,
+      function(error, result) {
+      do_check_eq(error, null);
+
+      // Step 4: check if the stats is deleted.
+      db.getStats("power", manifestURL, serviceType, component, start, end,
+        function(error, result) {
+        do_check_eq(result.statsData.length, 0);
+        run_next_test();
+      });
+    });
+  });
+});
+
+// Test clearing all network stats.
+add_test(function test_clearAllNetworkStats() {
+  db.clearAllStats("network", function(error, result) {
+    do_check_eq(error, null);
+    run_next_test();
+  });
+});
+
+// Test clearing all power stats.
+add_test(function test_clearAllPowerStats() {
+  db.clearAllStats("power", function(error, result) {
+    do_check_eq(error, null);
+    run_next_test();
+  });
+});
+
+// Test getting network components.
+add_test(function test_getNetworkComponents() {
+  var appId = 0;
+  var serviceType = "";
+
+  // Step 1: generate data saved to store.
+  var { record: record, result: expectedResult } =
+    generateNetworkRecord(appId, serviceType, networkComponents);
+  var recordArray = [record];
+  var timestamp = Date.now();
+
+  // Step 2: save recordArray to network store.
+  prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: call getComponents.
+    db.getComponents("network", function(error, result) {
+      do_check_eq(error, null);
+      do_check_true(Array.isArray(result));
+      do_check_eq(result.length, networkComponents.length);
+
+      // Check each element in result array is an element of networkComponents.
+      result.forEach(function(component) {
+        do_check_true(networkComponents.indexOf(component) > -1);
+      });
+
+      run_next_test(); // If success, run next test.
+    });
+  });
+});
+
+// Test getting power components.
+add_test(function test_getPowerComponents() {
+  var appId = 0;
+  var serviceType = "";
+
+  // Step 1: generate data saved to store.
+  var { record: record, result: expectedResult } =
+    generatePowerRecord(appId, serviceType, powerComponents);
+  var recordArray = [record];
+  var timestamp = Date.now();
+
+  // Step 2: save recordArray to power store.
+  preparePowerStatsStore(recordArray, timestamp, function(error, callback) {
+    // Step 3: call getComponents.
+    db.getComponents("power", function(error, result) {
+      do_check_eq(error, null);
+      do_check_true(Array.isArray(result));
+      do_check_eq(result.length, powerComponents.length);
+
+      // Check each element in result array is an element of powerComponents.
+      result.forEach(function(component) {
+        do_check_true(powerComponents.indexOf(component) > -1);
+      });
+
+      run_next_test(); // If success, run next test.
+    });
+  });
+});
+
+// Generate alarm object for addAlarm().
+function generateAlarmObject(aType, aManifestURL, aServiceType, aComponent) {
+  let alarm = {
+    type: aType,
+    component: aComponent,
+    serviceType: aServiceType,
+    manifestURL: aManifestURL,
+    threshold: Math.floor(Math.random() * 1000),
+    startTime: Math.floor(Math.random() * 1000),
+    data: null
+  };
+
+  return alarm;
+}
+
+// Test adding a network alarm.
+add_test(function test_addNetowrkAlarm() {
+  var manifestURL = "";
+  var serviceType = "";
+
+  // Step 1: generate a network alarm.
+  var alarm =
+    generateAlarmObject("network", manifestURL, serviceType, wifiComponent);
+
+  // Step 2: clear store.
+  clearStore("alarm_store", function() {
+    // Step 3: save the alarm to store.
+    db.addAlarm(alarm, function(error, result) {
+      // Step 4: check if the function call succeed.
+      do_check_eq(error, null);
+      do_check_true(result > -1);
+      let alarmId = result;
+
+      // Step 5: dump store for comparison.
+      dumpStore("alarm_store", function(error, result) {
+        do_check_eq(error, null);
+        do_check_true(Array.isArray(result));
+        do_check_true(result.length == 1);
+        do_check_eq(result[0].type, alarm.type);
+        do_check_eq(result[0].manifestURL, alarm.manifestURL);
+        do_check_eq(result[0].serviceType, alarm.serviceType);
+        do_check_eq(result[0].component, alarm.component);
+        do_check_eq(result[0].threshold, alarm.threshold);
+        do_check_eq(result[0].startTime, alarm.startTime);
+        do_check_eq(result[0].data, alarm.data);
+        do_check_eq(result[0].alarmId, alarmId);
+
+        run_next_test(); // If success, run next test.
+      });
+    });
+  });
+});
+
+// Test adding a power alarm.
+add_test(function test_addPowerAlarm() {
+  var manifestURL = "";
+  var serviceType = "";
+
+  // Step 1: generate a power alarm.
+  var alarm =
+    generateAlarmObject("power", manifestURL, serviceType, cpuComponent);
+
+  // Step 2: clear store.
+  clearStore("alarm_store", function() {
+    // Step 3: save the alarm to store.
+    db.addAlarm(alarm, function(error, result) {
+      // Step 4: check if the function call succeed.
+      do_check_eq(error, null);
+      do_check_true(result > -1);
+      let alarmId = result;
+
+      // Step 5: dump store for comparison.
+      dumpStore("alarm_store", function(error, result) {
+        do_check_eq(error, null);
+        do_check_true(Array.isArray(result));
+        do_check_true(result.length == 1);
+        do_check_eq(result[0].type, alarm.type);
+        do_check_eq(result[0].manifestURL, alarm.manifestURL);
+        do_check_eq(result[0].serviceType, alarm.serviceType);
+        do_check_eq(result[0].component, alarm.component);
+        do_check_eq(result[0].threshold, alarm.threshold);
+        do_check_eq(result[0].startTime, alarm.startTime);
+        do_check_eq(result[0].data, alarm.data);
+        do_check_eq(result[0].alarmId, alarmId);
+
+        run_next_test(); // If success, run next test.
+      });
+    });
+  });
+});
+
+// Add multiple alarms to store and record the obtained alarmId in each
+// alarm object.
+function addAlarmsToStore(alarms, index, callback) {
+  var alarm = alarms[index++];
+  if (index < alarms.length) {
+    db.addAlarm(alarm, function(error, result) {
+      alarm.alarmId = result;
+      addAlarmsToStore(alarms, index, callback);
+    });
+  } else {
+    db.addAlarm(alarm, function(error, result) {
+      alarm.alarmId = result;
+      callback(error, result);
+    });
+  }
+}
+
+// Prepare alarm store for testging.
+function prepareAlarmStore(alarms, callback) {
+  // Step 1: clear srore.
+  clearStore("alarm_store", function() {
+    // Step 2: save alarms to store one by one.
+    addAlarmsToStore(alarms, 0, callback);
+  });
+}
+
+// Compare alrams returned by getAlarms().
+function compareAlarms(aExpectedResult, aResult) {
+  // Step 1: a quick check for the length of arrays first.
+  do_check_eq(aExpectedResult.length, aResult.length);
+
+  // Step 2: create a map array for search by threshold.
+  var mapArray = aExpectedResult.map(function(e) {return e.threshold;});
+
+  // Step 3: compare each element to make sure both array are equal.
+  var index;
+  var target;
+
+  aResult.forEach(function(alarm) {
+    index = 0;
+    // Find the first equal receivedBytes since index.
+    while ((index = mapArray.indexOf(alarm.threshold, index)) > -1) {
+      // Compare all attributes.
+      target = aExpectedResult[index];
+      if (target.alarmId != alarm.alarmId ||
+          target.type != alarm.type ||
+          target.manifestURL != alarm.manifestURL ||
+          target.serviceType != alarm.serviceType ||
+          target.component != alarm.component ||
+          target.data != alarm.data) {
+        index += 1;
+        continue;
+      } else {
+        // If found, remove that element from aExpectedResult and mapArray.
+        aExpectedResult.splice(index, 1);
+        mapArray.splice(index, 1);
+        break;
+      }
+    }
+    do_check_neq(index, -1);
+  });
+  run_next_test();
+}
+
+// Test getting designate network alarms from store.
+add_test(function test_getNetworkAlarms() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate two network alarms using same parameters.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+
+  // Step 2: generate another network alarm using different parameters.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  mobileComponent));
+
+  // Step 3: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    // Step 4: call getAlarms.
+    let options = {
+      manifestURL: manifestURL,
+      serviceType: serviceType,
+      component: wifiComponent
+    };
+    db.getAlarms("network", options, function(error, result) {
+      // Step 5: check if the function call succeed.
+      do_check_eq(error, null);
+
+      // Step 6: check results.
+      // The last element in alarms array is not our expected result,
+      // so pop that out first.
+      alarms.pop();
+      compareAlarms(alarms, result);
+    });
+  });
+});
+
+// Test getting designate power alarms from store.
+add_test(function test_getPowerAlarms() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate two power alarms using same parameters.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+
+  // Step 2: generate another power alarm using different parameters.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  gpsComponent));
+
+  // Step 3: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    // Step 4: call getAlarms.
+    let options = {
+      manifestURL: manifestURL,
+      serviceType: serviceType,
+      component: cpuComponent
+    };
+    db.getAlarms("power", options, function(error, result) {
+      // Step 5: check if the function call succeed.
+      do_check_eq(error, null);
+
+      // Step 6: check results.
+      // The last element in alarms array is not our expected result,
+      // so pop that out first.
+      alarms.pop();
+      compareAlarms(alarms, result);
+    });
+  });
+});
+
+// Test getting all network alarms from store.
+add_test(function test_getAllNetworkAlarms() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate two network alarms.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  mobileComponent));
+
+  // Step 2: generate another power alarm.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+
+  // Step 3: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    // Step 4: call getAlarms.
+    let options = null;
+    db.getAlarms("network", options, function(error, result) {
+      // Step 5: check if the function call succeed.
+      do_check_eq(error, null);
+
+      // Step 6: check results.
+      // The last element in alarms array is not our expected result,
+      // so pop that out first.
+      alarms.pop();
+      compareAlarms(alarms, result);
+    });
+  });
+});
+
+// Test getting all power alarms from store.
+add_test(function test_getAllPowerAlarms() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate two power alarms.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  gpsComponent));
+
+  // Step 2: generate another network alarm.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+
+  // Step 3: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    // Step 4: call getAlarms.
+    let options = null;
+    db.getAlarms("power", options, function(error, result) {
+      // Step 5: check if the function call succeed.
+      do_check_eq(error, null);
+
+      // Step 6: check results.
+      // The last element in alarms array is not our expected result,
+      // so pop that out first.
+      alarms.pop();
+      compareAlarms(alarms, result);
+    });
+  });
+});
+
+// Test removing designate network alarm from store.
+add_test(function test_removeNetworkAlarm() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate one network alarm.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+
+  // Step 2: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    var alarmId = result;
+    // Step 3: remove the alarm.
+    db.removeAlarm("network", alarmId, function(error, result) {
+      // Step 4: check if the function call succeed.
+      do_check_eq(result, true);
+
+      // Step 5: dump store to check if the alarm is removed.
+      dumpStore("alarm_store", function(error, result) {
+        do_check_eq(error, null);
+        do_check_true(Array.isArray(result));
+        do_check_true(result.length === 0);
+
+        run_next_test(); // If success, run next test.
+      });
+    });
+  });
+});
+
+// Test removing designate power alarm from store.
+add_test(function test_removePowerAlarm() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate one power alarm.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+
+  // Step 2: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    var alarmId = result;
+    // Step 3: remove the alarm.
+    db.removeAlarm("power", alarmId, function(error, result) {
+      // Step 4: check if the function call succeed.
+      do_check_eq(result, true);
+
+      // Step 5: dump store to check if the alarm is removed.
+      dumpStore("alarm_store", function(error, result) {
+        do_check_eq(error, null);
+        do_check_true(Array.isArray(result));
+        do_check_true(result.length === 0);
+
+        run_next_test(); // If success, run next test.
+      });
+    });
+  });
+});
+
+// Test removing designate network alarm from store.
+add_test(function test_removeAllNetworkAlarms() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate two network alarms.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  mobileComponent));
+
+  // Step 2: generate another power alarm.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+
+  // Step 3: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    // Step 4: remove all network alarms.
+    db.removeAllAlarms("network", function(error, result) {
+      // Step 5: check if the function call succeed.
+      do_check_eq(error, null);
+
+      // Step 6: dump store for comparison.
+      // Because the power alarm should not be removed, so it would be the
+      // only result returned by dumpStore.
+      var alarm = alarms.pop(); // The expected result.
+      dumpStore("alarm_store", function(error, result) {
+        do_check_eq(error, null);
+        do_check_true(Array.isArray(result));
+        do_check_true(result.length == 1);
+        do_check_eq(result[0].type, alarm.type);
+        do_check_eq(result[0].manifestURL, alarm.manifestURL);
+        do_check_eq(result[0].serviceType, alarm.serviceType);
+        do_check_eq(result[0].component, alarm.component);
+        do_check_eq(result[0].threshold, alarm.threshold);
+        do_check_eq(result[0].startTime, alarm.startTime);
+        do_check_eq(result[0].data, alarm.data);
+        do_check_eq(result[0].alarmId, alarm.alarmId);
+
+        run_next_test(); // If success, run next test.
+      });
+    });
+  });
+});
+
+// Test removing designate power alarm from store.
+add_test(function test_removeAllPowerAlarms() {
+  var manifestURL = "";
+  var serviceType = "";
+  var alarms = [];
+
+  // Step 1: generate two power alarms.
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  cpuComponent));
+  alarms.push(generateAlarmObject("power", manifestURL, serviceType,
+                                  gpsComponent));
+
+  // Step 2: generate another network alarm.
+  alarms.push(generateAlarmObject("network", manifestURL, serviceType,
+                                  wifiComponent));
+
+  // Step 3: clear alarm store and save new alarms to store.
+  prepareAlarmStore(alarms, function(error, result) {
+    // Step 4: remove all power alarms.
+    db.removeAllAlarms("power", function(error, result) {
+      // Step 5: check if the function call succeed.
+      do_check_eq(error, null);
+
+      // Step 6: dump store for comparison.
+      // Because the network alarm should not be removed, so it would be the
+      // only result returned by dumpStore.
+      var alarm = alarms.pop(); // The expected result.
+      dumpStore("alarm_store", function(error, result) {
+        do_check_eq(error, null);
+        do_check_true(Array.isArray(result));
+        do_check_true(result.length == 1);
+        do_check_eq(result[0].type, alarm.type);
+        do_check_eq(result[0].manifestURL, alarm.manifestURL);
+        do_check_eq(result[0].serviceType, alarm.serviceType);
+        do_check_eq(result[0].component, alarm.component);
+        do_check_eq(result[0].threshold, alarm.threshold);
+        do_check_eq(result[0].startTime, alarm.startTime);
+        do_check_eq(result[0].data, alarm.data);
+        do_check_eq(result[0].alarmId, alarm.alarmId);
+
+        run_next_test(); // If success, run next test.
+      });
+    });
+  });
+});
+
+function run_test() {
+  do_get_profile();
+  run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_resourcestats_db.js]
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -2291,27 +2291,37 @@ RadioInterface.prototype = {
   },
 
   matchMvno: function(target, message) {
     if (DEBUG) this.debug("matchMvno: " + JSON.stringify(message));
 
     if (!message || !message.mvnoType || !message.mvnoData) {
       message.errorMsg = RIL.GECKO_ERROR_INVALID_PARAMETER;
     }
-    // Currently we only support imsi matching.
-    if (message.mvnoType != "imsi") {
-      message.errorMsg = RIL.GECKO_ERROR_MODE_NOT_SUPPORTED;
-    }
-    // Fire error if mvnoType is imsi but imsi is not available.
-    if (!this.rilContext.imsi) {
-      message.errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE;
-    }
 
     if (!message.errorMsg) {
-      message.result = this.isImsiMatches(message.mvnoData);
+      switch (message.mvnoType) {
+        case "imsi":
+          if (!this.rilContext.imsi) {
+            message.errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE;
+            break;
+          }
+          message.result = this.isImsiMatches(message.mvnoData);
+          break;
+        case "spn":
+          let spn = this.rilContext.iccInfo && this.rilContext.iccInfo.spn;
+          if (!spn) {
+            message.errorMsg = RIL.GECKO_ERROR_GENERIC_FAILURE;
+            break;
+          }
+          message.result = spn == message.mvnoData;
+          break;
+        default:
+          message.errorMsg = RIL.GECKO_ERROR_MODE_NOT_SUPPORTED;
+      }
     }
 
     target.sendAsyncMessage("RIL:MatchMvno", {
       clientId: this.clientId,
       data: message
     });
   },
 
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -14025,16 +14025,18 @@ ICCUtilsHelperObject.prototype = {
           let plmnMnc = iccSpdi[plmn].mnc;
           isOnMatchingPlmn = (plmnMcc == operatorMcc) && (plmnMnc == operatorMnc);
           if (isOnMatchingPlmn) {
             break;
           }
         }
       }
 
+      // See 3GPP TS 22.101 A.4 Service Provider Name indication, and TS 31.102
+      // clause 4.2.12 EF_SPN for detail.
       if (isOnMatchingPlmn) {
         // The first bit of display condition tells us if we should display
         // registered PLMN.
         if (DEBUG) {
           this.context.debug("PLMN is HPLMN or PLMN " + "is in PLMN list");
         }
 
         // TS 31.102 Sec. 4.2.66 and TS 51.011 Sec. 10.3.50
@@ -14044,20 +14046,17 @@ ICCUtilsHelperObject.prototype = {
         iccInfo.isDisplayNetworkNameRequired = (displayCondition & 0x01) !== 0;
       } else {
         // The second bit of display condition tells us if we should display
         // registered PLMN.
         if (DEBUG) {
           this.context.debug("PLMN isn't HPLMN and PLMN isn't in PLMN list");
         }
 
-        // We didn't found the requirement of displaying network name if
-        // current PLMN isn't HPLMN nor one of PLMN in SPDI. So we keep
-        // isDisplayNetworkNameRequired false.
-        iccInfo.isDisplayNetworkNameRequired = false;
+        iccInfo.isDisplayNetworkNameRequired = true;
         iccInfo.isDisplaySpnRequired = (displayCondition & 0x02) === 0;
       }
     }
 
     if (DEBUG) {
       this.context.debug("isDisplayNetworkNameRequired = " +
                          iccInfo.isDisplayNetworkNameRequired);
       this.context.debug("isDisplaySpnRequired = " + iccInfo.isDisplaySpnRequired);
--- a/dom/system/gonk/tests/test_ril_worker_icc_SimRecordHelper.js
+++ b/dom/system/gonk/tests/test_ril_worker_icc_SimRecordHelper.js
@@ -481,9 +481,124 @@ add_test(function test_read_new_sms_on_s
     do_check_eq("How are you?", postedMessage.body);
   }
 
   do_test();
 
   run_next_test();
 });
 
+/**
+ * Verify the result of updateDisplayCondition after reading EF_SPDI | EF_SPN.
+ */
+add_test(function test_update_display_condition() {
+  let worker = newUint8Worker();
+  let context = worker.ContextPool._contexts[0];
+  let record = context.SimRecordHelper;
+  let helper = context.GsmPDUHelper;
+  let ril    = context.RIL;
+  let buf    = context.Buf;
+  let io     = context.ICCIOHelper;
 
+  function do_test_spdi() {
+    // No EF_SPN, but having EF_SPDI.
+    // It implies "ril.iccInfoPrivate.spnDisplayCondition = undefined;".
+    io.loadTransparentEF = function fakeLoadTransparentEF(options) {
+      // PLMN lists are : 234-136 and 466-92.
+      let spdi = [0xA3, 0x0B, 0x80, 0x09, 0x32, 0x64, 0x31, 0x64, 0x26, 0x9F,
+                  0xFF, 0xFF, 0xFF];
+
+      // Write data size.
+      buf.writeInt32(spdi.length * 2);
+
+      // Write data.
+      for (let i = 0; i < spdi.length; i++) {
+        helper.writeHexOctet(spdi[i]);
+      }
+
+      // Write string delimiter.
+      buf.writeStringDelimiter(spdi.length * 2);
+
+      if (options.callback) {
+        options.callback(options);
+      }
+    };
+
+    record.readSPDI();
+
+    do_check_eq(ril.iccInfo.isDisplayNetworkNameRequired, true);
+    do_check_eq(ril.iccInfo.isDisplaySpnRequired, false);
+  }
+
+  function do_test_spn(displayCondition,
+                       expectedPlmnNameDisplay,
+                       expectedSpnDisplay) {
+    io.loadTransparentEF = function fakeLoadTransparentEF(options) {
+      // "Android" as Service Provider Name.
+      let spn = [0x41, 0x6E, 0x64, 0x72, 0x6F, 0x69, 0x64];
+      if (typeof displayCondition === 'number') {
+        spn.unshift(displayCondition);
+      }
+
+      // Write data size.
+      buf.writeInt32(spn.length * 2);
+
+      // Write data.
+      for (let i = 0; i < spn.length; i++) {
+        helper.writeHexOctet(spn[i]);
+      }
+
+      // Write string delimiter.
+      buf.writeStringDelimiter(spn.length * 2);
+
+      if (options.callback) {
+        options.callback(options);
+      }
+    };
+
+    record.readSPN();
+
+    do_check_eq(ril.iccInfo.isDisplayNetworkNameRequired, expectedPlmnNameDisplay);
+    do_check_eq(ril.iccInfo.isDisplaySpnRequired, expectedSpnDisplay);
+  }
+
+  // Create empty operator object.
+  ril.operator = {};
+  // Setup SIM MCC-MNC to 310-260 as home network.
+  ril.iccInfo.mcc = 310;
+  ril.iccInfo.mnc = 260;
+
+  do_test_spdi();
+
+  // No network.
+  do_test_spn(0x00, true, true);
+  do_test_spn(0x01, true, true);
+  do_test_spn(0x02, true, false);
+  do_test_spn(0x03, true, false);
+
+  // Home network.
+  ril.operator.mcc = 310;
+  ril.operator.mnc = 260;
+  do_test_spn(0x00, false, true);
+  do_test_spn(0x01, true, true);
+  do_test_spn(0x02, false, true);
+  do_test_spn(0x03, true, true);
+
+  // Not HPLMN but in PLMN list.
+  ril.iccInfoPrivate.SPDI = [{"mcc":"234","mnc":"136"},{"mcc":"466","mnc":"92"}];
+  ril.operator.mcc = 466;
+  ril.operator.mnc = 92;
+  do_test_spn(0x00, false, true);
+  do_test_spn(0x01, true, true);
+  do_test_spn(0x02, false, true);
+  do_test_spn(0x03, true, true);
+  ril.iccInfoPrivate.SPDI = null; // reset SPDI to null;
+
+  // Non-Home network.
+  ril.operator.mcc = 466;
+  ril.operator.mnc = 01;
+  do_test_spn(0x00, true, true);
+  do_test_spn(0x01, true, true);
+  do_test_spn(0x02, true, false);
+  do_test_spn(0x03, true, false);
+
+  run_next_test();
+});
--- a/dom/webidl/BluetoothAdapter2.webidl
+++ b/dom/webidl/BluetoothAdapter2.webidl
@@ -81,56 +81,61 @@ interface BluetoothAdapter : EventTarget
            attribute EventHandler   onscostatuschanged;
 
   // Fired when remote devices query current media play status
            attribute EventHandler   onrequestmediaplaystatus;
 
   // Fired when attributes of BluetoothAdapter changed
            attribute EventHandler   onattributechanged;
 
+  /**
+   * Enable/Disable a local bluetooth adapter by asynchronus methods and return
+   * its result through a Promise.
+   *
+   * Several onattributechanged events would be triggered during processing the
+   * request, and the last one would indicate adapter.state becomes
+   * enabled/disabled.
+   */
   // Promise<void>
-  [Throws]
+  [NewObject, Throws]
+  Promise enable();
+  // Promise<void>
+  [NewObject, Throws]
+  Promise disable();
+
+  // Promise<void>
+  [NewObject, Throws]
   Promise setName(DOMString aName);
   // Promise<void>
-  [Throws]
+  [NewObject, Throws]
   Promise setDiscoverable(boolean aDiscoverable);
+
+  // Promise<BluetoothDiscoveryHandle>
   [NewObject, Throws]
-  DOMRequest startDiscovery();
+  Promise startDiscovery();
+  // Promise<void>
   [NewObject, Throws]
-  DOMRequest stopDiscovery();
+  Promise stopDiscovery();
+
   [NewObject, Throws]
   DOMRequest pair(DOMString deviceAddress);
   [NewObject, Throws]
   DOMRequest unpair(DOMString deviceAddress);
   [NewObject, Throws]
   DOMRequest getPairedDevices();
   [NewObject, Throws]
   DOMRequest getConnectedDevices(unsigned short serviceUuid);
   [NewObject, Throws]
   DOMRequest setPinCode(DOMString deviceAddress, DOMString pinCode);
   [NewObject, Throws]
   DOMRequest setPasskey(DOMString deviceAddress, unsigned long passkey);
   [NewObject, Throws]
   DOMRequest setPairingConfirmation(DOMString deviceAddress, boolean confirmation);
 
   /**
-   * Enable/Disable a local bluetooth adapter by asynchronus methods and return
-   * its result through a Promise.
-   * Several onattributechanged event would be triggered during processing the
-   * request, and the last one would indicate adapter.state becomes
-   * enabled/disabled.
-   */
-  // Promise<void>
-  [Throws]
-  Promise enable();
-  // Promise<void>
-  [Throws]
-  Promise disable();
-
-  /**
    * Connect/Disconnect to a specific service of a target remote device.
    * To check the value of service UUIDs, please check "Bluetooth Assigned
    * Numbers" / "Service Discovery Protocol" for more information.
    *
    * Note that service UUID is optional. If it isn't passed when calling
    * Connect, multiple profiles are tried sequentially based on the class of
    * device (CoD). If it isn't passed when calling Disconnect, all connected
    * profiles are going to be closed.
--- a/dom/webidl/BluetoothDeviceEvent.webidl
+++ b/dom/webidl/BluetoothDeviceEvent.webidl
@@ -1,17 +1,19 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
-[Constructor(DOMString type, optional BluetoothDeviceEventInit eventInitDict),
- CheckPermissions="bluetooth"]
+[CheckPermissions="bluetooth",
+ Constructor(DOMString type, optional BluetoothDeviceEventInit eventInitDict)]
 interface BluetoothDeviceEvent : Event
 {
   readonly attribute BluetoothDevice? device;
+  readonly attribute DOMString?       address;
 };
 
 dictionary BluetoothDeviceEventInit : EventInit
 {
   BluetoothDevice? device = null;
+  DOMString?       address = "";
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/BluetoothDiscoveryHandle.webidl
@@ -0,0 +1,9 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+[CheckPermissions="bluetooth"]
+interface BluetoothDiscoveryHandle : EventTarget {
+  attribute EventHandler ondevicefound;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/ResourceStats.webidl
@@ -0,0 +1,71 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+[CheckPermissions="resourcestats-manage",
+ Pref="dom.resource_stats.enabled",
+ AvailableIn="CertifiedApps",
+// FeatureDetectible, // This should be specified after Bug 1009645 is resolved.
+ JSImplementation="@mozilla.org/networkStatsData;1"]
+interface NetworkStatsData
+{
+  readonly attribute unsigned long long   receivedBytes;
+  readonly attribute unsigned long long   sentBytes;
+  readonly attribute DOMTimeStamp         timestamp;      // timestamp of the record
+};
+
+[CheckPermissions="resourcestats-manage",
+ Pref="dom.resource_stats.enabled",
+ AvailableIn="CertifiedApps",
+// FeatureDetectible, // This should be specified after Bug 1009645 is resolved.
+ JSImplementation="@mozilla.org/powerStatsData;1"]
+interface PowerStatsData
+{
+  readonly attribute unsigned long long   consumedPower;  // unit: mW
+  readonly attribute DOMTimeStamp         timestamp;      // timestamp of the record
+};
+
+[CheckPermissions="resourcestats-manage",
+ Pref="dom.resource_stats.enabled",
+ AvailableIn="CertifiedApps",
+// FeatureDetectible, // This should be specified after Bug 1009645 is resolved.
+ JSImplementation="@mozilla.org/resourceStats;1"]
+interface ResourceStats
+{
+  /**
+   * Type of statistics/
+   */
+  readonly attribute ResourceType   type;
+
+  /**
+   * The |component| specifies statistics belongs to. This will be null if
+   * the ResourceStatsOptions.component argument passed to getStats() is null.
+   */
+  readonly attribute DOMString?     component;
+
+  /**
+   * |serviceType| specifies the system service. This will be null if the
+   * ResourceStatsOptions.serviceType argument passed to getStats() is null.
+   */
+  readonly attribute SystemService? serviceType;
+
+  /**
+   * |manifestURL| specifies the manifestURL of an application. This will be
+   * null if the ResourceStatsOptions.manifestURL argument passed to getStats()
+   * is null.
+   */
+  readonly attribute DOMString?     manifestURL;
+
+  /**
+   * Statistics, one element per day
+   */
+  sequence<(NetworkStatsData or PowerStatsData)> getData();
+
+  /**
+   * Date range
+   */
+  readonly attribute DOMTimeStamp   start;  // start timestamp
+  readonly attribute DOMTimeStamp   end;    // end timestamp
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/ResourceStatsManager.webidl
@@ -0,0 +1,226 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Supported resource statistics
+ */
+enum ResourceType {
+  "network",
+  "power"
+};
+
+/**
+ * List of system services supporting resource statistics
+ */
+enum SystemService {
+  "ota",
+  "tethering"
+};
+
+dictionary ResourceStatsOptions
+{
+  /**
+   * |component| specifies which component's resource usage will be returned.
+   * If null or undefined, sum of all components' usage is returned.
+   *
+   * |component| is expressed in "<component>:<id>", where <component> is the
+   * name of the component and <id> is used to identify different entities.
+   *
+   * The <id> field is mainly used in specifying the identity of different SIMs
+   * when quering mobile network usage, e.g. "mobile:<iccid>".
+   *
+   * Quering statistics of other components should specify the |component| to
+   *  "<component>:0", such as "cpu:0" or "wifi:0".
+   */
+  DOMString? component = null;
+
+  /**
+   * |manifestURL| specifies the manifestURL of an application.
+   * |systemService| specifies the system service.
+   *
+   * If both |systemService| and |manifestURL| are null or undefined, then a
+   * system-wide resource statistics is returned.
+   *
+   * If |manifestURL| is specified, then the resource statistics of the
+   * specified application is returned.
+   *
+   * If |systemService| is specified, then the resource statistics of the
+   * specified system service is returned.
+   *
+   * If |systemService| and |manifestURL| are both specified, then the return
+   * statistics indicates the resources that the system service consumed for
+   * the application.
+   */
+  SystemService? serviceType = null;
+  DOMString? manifestURL = null;
+};
+
+dictionary ResourceStatsAlarmOptions
+{
+  /**
+   * |startTime| indicates the start time of counting the resource usage.
+   *
+   * |data| is used to reflect in the alarm object when the alarm is triggered.
+   * |data| should be copied using the structured clone algorithm.
+   */
+  [EnforceRange] DOMTimeStamp   startTime;  // time in milliseconds since Epoch
+  any                           data;
+};
+
+[CheckPermissions="resourcestats-manage",
+ Pref="dom.resource_stats.enabled",
+ AvailableIn="CertifiedApps",
+// FeatureDetectible, // This should be specified after Bug 1009645 is resolved.
+ JSImplementation="@mozilla.org/resourceStatsAlarm;1"]
+interface ResourceStatsAlarm
+{
+  /**
+   * ID of the alarm
+   */
+  readonly attribute unsigned long          alarmId;
+
+  /**
+   * Type of resource this alarm monitor
+   */
+  readonly attribute ResourceType           type;
+
+  /**
+   * The target component this alarm monitor.
+   */
+  readonly attribute DOMString?             component;
+
+  /**
+   * |manifestURL| specifies the manifestURL of an application.
+   * |systemService| specifies the system service.
+   *
+   * Both attributes are null means that this alarm monitors a system-wide
+   * resource usage.
+   */
+  readonly attribute SystemService?         serviceType;
+  readonly attribute DOMString?             manifestURL;
+
+  /**
+   * |threshold| specifies the limit of resource usage.
+   */
+  readonly attribute unsigned long long     threshold;
+
+  /**
+   * |data| is used to reflect in the alarm object when the alarm is triggered.
+   */
+  readonly attribute any                    data;
+};
+
+[CheckPermissions="resourcestats-manage",
+ Pref="dom.resource_stats.enabled",
+ Constructor(ResourceType type),
+ AvailableIn="CertifiedApps",
+// FeatureDetectible, // This should be specified after Bug 1009645 is resolved.
+ JSImplementation="@mozilla.org/resourceStatsManager;1"]
+interface ResourceStatsManager
+{
+  /**
+   * Query resource statistics.
+   *
+   * |statsOptions| specifies the detail of statistics of interest.
+   *
+   * |start| and |end| specifies the time range of interest, both included.
+   * If |start| is null or undefined, retrieve the stats since measurements.
+   * If |end| is null or undefined. retrieve the stats until the current time.
+   *
+   * If success, the fulfillment value is a ResourceStats object.
+   */
+  Promise getStats(optional ResourceStatsOptions statsOptions,
+                   [EnforceRange] optional DOMTimeStamp? start = null,
+                   [EnforceRange] optional DOMTimeStamp? end = null);
+
+  /**
+   * Clear resource statistics stored in database.
+   *
+   * |statsOptions| specifies the detail of statistics to delete.
+   *
+   * |start| and |end| specifies the time range of interest, both included.
+   * If |start| is null or undefined, delete the stats since measurements.
+   * If |end| is null or undefined. delete the stats until the current time.
+   */
+  Promise clearStats(optional ResourceStatsOptions statsOptions,
+                     [EnforceRange] optional DOMTimeStamp? start = null,
+                     [EnforceRange] optional DOMTimeStamp? end = null);
+
+  /**
+   * Clear all resource statistics stored in database.
+   */
+  Promise clearAllStats();
+
+  /**
+   * Install an alarm to monitor resource usage.
+   *
+   * The |threshold| specifies the limit of resource usage. When resource
+   * usage reaches the threshold, a "resourceStats-alarm" system message
+   * is sent to the application.
+   *
+   * |statsOptions| specifies the detail of statistics of interest.
+   *
+   * |alarmOptions| is a ResourceStatsAlarmOptions object.
+   *
+   * If success, the fulfillment value is an alarm ID.
+   */
+  Promise addAlarm([EnforceRange] unsigned long long threshold,
+                   optional ResourceStatsOptions statsOptions,
+                   optional ResourceStatsAlarmOptions alarmOptions);
+
+  /**
+   * Obtain alarms.
+   *
+   * If |statsOptions| is provided, then only the alarms monitoring that
+   * resource are returned. Otherwise, all alarms set for this resource type
+   * is returned.
+   *
+   * If success, the fulfillment value is an array of ResourceStatsAlarm.
+   */
+  Promise getAlarms(optional ResourceStatsOptions statsOptions);
+
+  /**
+   * Remove the specified alarm.
+   *
+   * |alarmId| specifies the alarm to be removed.
+   */
+  Promise removeAlarm([EnforceRange] unsigned long alarmId);
+
+  /**
+   * Remove all alarms.
+   */
+  Promise removeAllAlarms();
+
+  /**
+   * Enumerate components that have stored statistics in database.
+   *
+   * If success, the fulfillment value is an array of DOMString.
+   */
+  Promise getAvailableComponents();
+
+  /**
+   * Return supporting resource statistics, i.e. ["Network", "Power"]
+   *
+   * This should be specified as static attribute after Bug 863952 is resolved.
+   */
+  [Cached, Pure]
+  readonly attribute sequence<DOMString> resourceTypes;
+
+  /**
+   * Time in milliseconds between statistics stored in database.
+   *
+   * This should be specified as static attribute after Bug 863952 is resolved.
+   */
+  readonly attribute unsigned long sampleRate;
+
+  /**
+   * Time in milliseconds recorded by the API until present time. All
+   * statistics older than maxStorageAge from now are deleted.
+   *
+   * This should be specified as static attribute after Bug 863952 is resolved.
+   */
+  readonly attribute unsigned long long maxStorageAge;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -296,16 +296,18 @@ WEBIDL_FILES = [
     'Position.webidl',
     'PositionError.webidl',
     'ProcessingInstruction.webidl',
     'Promise.webidl',
     'PromiseDebugging.webidl',
     'PushManager.webidl',
     'Range.webidl',
     'Rect.webidl',
+    'ResourceStats.webidl',
+    'ResourceStatsManager.webidl',
     'RGBColor.webidl',
     'RTCConfiguration.webidl',
     'RTCIceCandidate.webidl',
     'RTCIdentityAssertion.webidl',
     'RTCPeerConnection.webidl',
     'RTCPeerConnectionStatic.webidl',
     'RTCSessionDescription.webidl',
     'RTCStatsReport.webidl',
@@ -557,16 +559,17 @@ WEBIDL_FILES += [
 if CONFIG['MOZ_DEBUG']:
     WEBIDL_FILES += ['TestInterfaceJS.webidl']
 
 if CONFIG['MOZ_B2G_BT']:
     if CONFIG['MOZ_B2G_BT_API_V2']:
         WEBIDL_FILES += [
             'BluetoothAdapter2.webidl',
             'BluetoothDevice2.webidl',
+            'BluetoothDiscoveryHandle.webidl',
             'BluetoothManager2.webidl',
         ]
     else:
         WEBIDL_FILES += [
             'BluetoothAdapter.webidl',
             'BluetoothDevice.webidl',
             'BluetoothManager.webidl',
         ]
--- a/dom/wifi/WifiWorker.js
+++ b/dom/wifi/WifiWorker.js
@@ -2500,16 +2500,17 @@ WifiWorker.prototype = {
                      ipAddress: self.ipAddress };
         let last = self._lastConnectionInfo;
 
         // Only fire the event if the link speed changed or the signal
         // strength changed by more than 10%.
         function tensPlace(percent) ((percent / 10) | 0)
 
         if (last && last.linkSpeed === info.linkSpeed &&
+            last.ipAddress === info.ipAddress &&
             tensPlace(last.relSignalStrength) === tensPlace(info.relSignalStrength)) {
           return;
         }
 
         self._lastConnectionInfo = info;
         debug("Firing connectionInfoUpdate: " + uneval(info));
         self._fireEvent("connectionInfoUpdate", info);
       });
--- a/hal/Hal.cpp
+++ b/hal/Hal.cpp
@@ -1234,10 +1234,16 @@ GetTotalSystemMemory()
 }
 
 uint32_t
 GetTotalSystemMemoryLevel()
 {
   return hal_impl::GetTotalSystemMemoryLevel();
 }
 
+bool IsHeadphoneEventFromInputDev()
+{
+  AssertMainThread();
+  RETURN_PROXY_IF_SANDBOXED(IsHeadphoneEventFromInputDev(), false);
+}
+
 } // namespace hal
 } // namespace mozilla
--- a/hal/Hal.h
+++ b/hal/Hal.h
@@ -611,16 +611,21 @@ uint32_t GetTotalSystemMemory();
 /**
  * Get the level of total system memory on device in MiB.
  * (round the value up to the next power of two)
  *
  * Returns 0 if we are unable to determine this information from /proc/meminfo.
  */
 uint32_t GetTotalSystemMemoryLevel();
 
+/**
+ * Determine whether the headphone switch event is from input device
+ */
+bool IsHeadphoneEventFromInputDev();
+
 } // namespace MOZ_HAL_NAMESPACE
 } // namespace mozilla
 
 #ifdef MOZ_DEFINED_HAL_NAMESPACE
 # undef MOZ_DEFINED_HAL_NAMESPACE
 # undef MOZ_HAL_NAMESPACE
 #endif
 
--- a/hal/fallback/FallbackSwitch.cpp
+++ b/hal/fallback/FallbackSwitch.cpp
@@ -25,10 +25,15 @@ GetCurrentSwitchState(SwitchDevice aDevi
   return SWITCH_STATE_UNKNOWN;
 }
 
 void
 NotifySwitchStateFromInputDevice(SwitchDevice aDevice, SwitchState aState)
 {
 }
 
+bool IsHeadphoneEventFromInputDev()
+{
+  return false;
+}
+
 } // namespace hal_impl
 } // namespace mozilla
--- a/hal/gonk/GonkSwitch.cpp
+++ b/hal/gonk/GonkSwitch.cpp
@@ -12,17 +12,16 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 #include <android/log.h>
 #include <fcntl.h>
 #include <sysutils/NetlinkEvent.h>
-#include <cutils/properties.h>
 
 #include "base/message_loop.h"
 
 #include "Hal.h"
 #include "mozilla/FileUtils.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Monitor.h"
 #include "nsPrintfCString.h"
@@ -235,17 +234,19 @@ class SwitchEventObserver MOZ_FINAL : pu
 {
   ~SwitchEventObserver()
   {
     mHandler.Clear();
   }
 
 public:
   NS_INLINE_DECL_REFCOUNTING(SwitchEventObserver)
-  SwitchEventObserver() : mEnableCount(0)
+  SwitchEventObserver()
+    : mEnableCount(0),
+    mHeadphonesFromInputDev(false)
   {
     Init();
   }
 
   int GetEnableCount()
   {
     return mEnableCount;
   }
@@ -304,41 +305,50 @@ public:
 
   void NotifyAnEvent(SwitchDevice aDevice)
   {
     EventInfo& info = mEventInfo[aDevice];
     if (info.mEvent.status() != SWITCH_STATE_UNKNOWN) {
       NS_DispatchToMainThread(new SwitchEventRunnable(info.mEvent));
     }
   }
+
+  bool GetHeadphonesFromInputDev()
+  {
+    return mHeadphonesFromInputDev;
+  }
+
 private:
   class EventInfo
   {
   public:
     EventInfo() : mEnabled(false)
     {
       mEvent.status() = SWITCH_STATE_UNKNOWN;
       mEvent.device() = SWITCH_DEVICE_UNKNOWN;
     }
     SwitchEvent mEvent;
     bool mEnabled;
   };
 
   EventInfo mEventInfo[NUM_SWITCH_DEVICE];
   size_t mEnableCount;
   SwitchHandlerArray mHandler;
+  bool mHeadphonesFromInputDev;
 
   void Init()
   {
-    char value[PROPERTY_VALUE_MAX];
-    property_get("ro.moz.devinputjack", value, "0");
-    bool headphonesFromInputDev = !strcmp(value, "1");
+    RefPtr<SwitchHandlerHeadphone> switchHeadPhone =
+      new SwitchHandlerHeadphone(SWITCH_HEADSET_DEVPATH);
 
-    if (!headphonesFromInputDev) {
-      mHandler.AppendElement(new SwitchHandlerHeadphone(SWITCH_HEADSET_DEVPATH));
+    // If the initial state is unknown, it means the headphone event is from input dev
+    mHeadphonesFromInputDev = switchHeadPhone->GetState() == SWITCH_STATE_UNKNOWN ? true : false;
+
+    if (!mHeadphonesFromInputDev) {
+      mHandler.AppendElement(switchHeadPhone);
     } else {
       // If headphone status will be notified from input dev then initialize
       // status to "off" and wait for event notification.
       mEventInfo[SWITCH_HEADPHONES].mEvent.device() = SWITCH_HEADPHONES;
       mEventInfo[SWITCH_HEADPHONES].mEvent.status() = SWITCH_STATE_OFF;
     }
     mHandler.AppendElement(new SwitchHandler(SWITCH_USB_DEVPATH_GB, SWITCH_USB));
     mHandler.AppendElement(new SwitchHandlerUsbIcs(SWITCH_USB_DEVPATH_ICS));
@@ -453,10 +463,17 @@ NotifySwitchStateIOThread(SwitchDevice a
 
 void NotifySwitchStateFromInputDevice(SwitchDevice aDevice, SwitchState aState)
 {
   InitializeResourceIfNeed();
   XRE_GetIOMessageLoop()->PostTask(
       FROM_HERE,
       NewRunnableFunction(NotifySwitchStateIOThread, aDevice, aState));
 }
+
+bool IsHeadphoneEventFromInputDev()
+{
+  InitializeResourceIfNeed();
+  return sSwitchObserver->GetHeadphonesFromInputDev();
+}
+
 } // hal_impl
 } //mozilla
--- a/hal/sandbox/SandboxHal.cpp
+++ b/hal/sandbox/SandboxHal.cpp
@@ -438,16 +438,22 @@ StartDiskSpaceWatcher()
 }
 
 void
 StopDiskSpaceWatcher()
 {
   NS_RUNTIMEABORT("StopDiskSpaceWatcher() can't be called from sandboxed contexts.");
 }
 
+bool IsHeadphoneEventFromInputDev()
+{
+  NS_RUNTIMEABORT("IsHeadphoneEventFromInputDev() cannot be called from sandboxed contexts.");
+  return false;
+}
+
 class HalParent : public PHalParent
                 , public BatteryObserver
                 , public NetworkObserver
                 , public ISensorObserver
                 , public WakeLockObserver
                 , public ScreenConfigurationObserver
                 , public SwitchObserver
                 , public SystemClockChangeObserver
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2090,16 +2090,18 @@ public class GeckoAppShell
             return null;
         }
     }
 
     private static ContextGetter sContextGetter;
 
     @WrapElementForJNI(allowMultithread = true)
     public static Context getContext() {
+        if (sContextGetter == null)
+            return null;
         return sContextGetter.getContext();
     }
 
     public static void setContextGetter(ContextGetter cg) {
         sContextGetter = cg;
     }
 
     public static SharedPreferences getSharedPreferences() {
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -97,16 +97,18 @@ public class GeckoApplication extends Ap
                 }
             });
         }
         GeckoConnectivityReceiver.getInstance().stop();
         GeckoNetworkManager.getInstance().stop();
     }
 
     public void onActivityResume(GeckoActivityStatus activity) {
+	GeckoAppShell.setContextGetter(this);
+
         if (mPausedGecko) {
             GeckoAppShell.sendEventToGecko(GeckoEvent.createAppForegroundingEvent());
             mPausedGecko = false;
         }
 
         final Context applicationContext = getApplicationContext();
         GeckoBatteryManager.getInstance().start(applicationContext);
         GeckoConnectivityReceiver.getInstance().start(applicationContext);
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -28,16 +28,19 @@ public class BrowserContract {
     public static final Uri HOME_AUTHORITY_URI = Uri.parse("content://" + HOME_AUTHORITY);
 
     public static final String PROFILES_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".profiles";
     public static final Uri PROFILES_AUTHORITY_URI = Uri.parse("content://" + PROFILES_AUTHORITY);
 
     public static final String READING_LIST_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.readinglist";
     public static final Uri READING_LIST_AUTHORITY_URI = Uri.parse("content://" + READING_LIST_AUTHORITY);
 
+    public static final String SEARCH_HISTORY_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.searchhistory";
+    public static final Uri SEARCH_HISTORY_AUTHORITY_URI = Uri.parse("content://" + SEARCH_HISTORY_AUTHORITY);
+
     public static final String PARAM_PROFILE = "profile";
     public static final String PARAM_PROFILE_PATH = "profilePath";
     public static final String PARAM_LIMIT = "limit";
     public static final String PARAM_IS_SYNC = "sync";
     public static final String PARAM_SHOW_DELETED = "show_deleted";
     public static final String PARAM_IS_TEST = "test";
     public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
     public static final String PARAM_INCREMENT_VISITS = "increment_visits";
@@ -429,14 +432,25 @@ public class BrowserContract {
         public static final String BOOKMARK_ID = "bookmark_id";
         public static final String HISTORY_ID = "history_id";
         public static final String DISPLAY = "display";
 
         public static final String TYPE = "type";
     }
 
     @RobocopTarget
+    public static final class SearchHistory implements CommonColumns, HistoryColumns {
+        private SearchHistory() {}
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searchhistory";
+        public static final String QUERY = "query";
+        public static final String TABLE_NAME = "searchhistory";
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(SEARCH_HISTORY_AUTHORITY_URI, "searchhistory");
+    }
+
+    @RobocopTarget
     public static final class SuggestedSites implements CommonColumns, URLColumns {
         private SuggestedSites() {}
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
     }
 }
--- a/mobile/android/base/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/db/BrowserDatabaseHelper.java
@@ -11,16 +11,17 @@ import java.util.List;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Obsolete;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.sync.Utils;
 
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.SQLException;
@@ -29,17 +30,17 @@ import android.database.sqlite.SQLiteOpe
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
 
 
 final class BrowserDatabaseHelper extends SQLiteOpenHelper {
 
     private static final String LOGTAG = "GeckoBrowserDBHelper";
-    public static final int DATABASE_VERSION = 18;
+    public static final int DATABASE_VERSION = 19;
     public static final String DATABASE_NAME = "browser.db";
 
     final protected Context mContext;
 
     static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
     static final String TABLE_HISTORY = History.TABLE_NAME;
     static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
     static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
@@ -744,16 +745,30 @@ final class BrowserDatabaseHelper extend
         createHistoryWithFaviconsView(db);
         createCombinedViewOn16(db);
 
         createOrUpdateSpecialFolder(db, Bookmarks.PLACES_FOLDER_GUID,
             R.string.bookmarks_folder_places, 0);
 
         createOrUpdateAllSpecialFolders(db);
         createReadingListTable(db);
+        createSearchHistoryTable(db);
+    }
+
+    private void createSearchHistoryTable(SQLiteDatabase db) {
+        debug("Creating " + SearchHistory.TABLE_NAME + " table");
+
+        db.execSQL("CREATE TABLE " + SearchHistory.TABLE_NAME + "(" +
+                    SearchHistory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                    SearchHistory.QUERY + " TEXT UNIQUE NOT NULL, " +
+                    SearchHistory.DATE_LAST_VISITED + " INTEGER, " +
+                    SearchHistory.VISITS + " INTEGER ) ");
+
+        db.execSQL("CREATE INDEX idx_search_history_last_visited ON " +
+                SearchHistory.TABLE_NAME + "(" + SearchHistory.DATE_LAST_VISITED + ")");
     }
 
     private void createReadingListTable(SQLiteDatabase db) {
         debug("Creating " + TABLE_READING_LIST + " table");
 
         db.execSQL("CREATE TABLE " + TABLE_READING_LIST + "(" +
                     ReadingListItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                     ReadingListItems.URL + " TEXT NOT NULL, " +
@@ -1373,16 +1388,20 @@ final class BrowserDatabaseHelper extend
         } finally {
             if (cursor != null) {
                 cursor.close();
             }
             db.endTransaction();
         }
     }
 
+    private void upgradeDatabaseFrom18to19(SQLiteDatabase db) {
+        createSearchHistoryTable(db);
+    }
+
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         debug("Upgrading browser.db: " + db.getPath() + " from " +
                 oldVersion + " to " + newVersion);
 
         // We have to do incremental upgrades until we reach the current
         // database schema version.
         for (int v = oldVersion + 1; v <= newVersion; v++) {
@@ -1449,16 +1468,20 @@ final class BrowserDatabaseHelper extend
 
                 case 17:
                     upgradeDatabaseFrom16to17(db);
                     break;
 
                 case 18:
                     upgradeDatabaseFrom17to18(db);
                     break;
+
+                case 19:
+                    upgradeDatabaseFrom18to19(db);
+                    break;
             }
         }
 
         // If an upgrade after 12->13 fails, the entire upgrade is rolled
         // back, but we can't undo the deletion of favicon_urls.db if we
         // delete this in step 13; therefore, we wait until all steps are
         // complete before removing it.
         if (oldVersion < 13 && newVersion >= 13
@@ -1561,9 +1584,9 @@ final class BrowserDatabaseHelper extend
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_BOOKMARK);
             } else {
                 bookmark.put(Bookmarks.TYPE, Bookmarks.TYPE_FOLDER);
             }
 
             bookmark.remove("folder");
         }
     }
-}
\ No newline at end of file
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/SearchHistoryProvider.java
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.db;
+
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public class SearchHistoryProvider extends SharedBrowserDatabaseProvider {
+
+    /**
+     * Collapse whitespace.
+     */
+    private String stripWhitespace(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return "";
+        }
+
+        // Collapse whitespace
+        return query.trim().replaceAll("\\s+", " ");
+    }
+
+
+    @Override
+    public Uri insertInTransaction(Uri uri, ContentValues cv) {
+        final String query = stripWhitespace(cv.getAsString(SearchHistory.QUERY));
+
+        // We don't support inserting empty search queries.
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        /*
+         * FIRST: Try incrementing the VISITS counter and updating the DATE_LAST_VISITED.
+         */
+        final String sql = "UPDATE " + SearchHistory.TABLE_NAME + " SET " +
+                SearchHistory.VISITS + " = " + SearchHistory.VISITS + " + 1, " +
+                SearchHistory.DATE_LAST_VISITED + " = " + System.currentTimeMillis() +
+                " WHERE " + SearchHistory.QUERY + " = ?";
+        final Cursor c = db.rawQuery(sql, new String[] { query });
+
+        try {
+            if (c.getCount() > 1) {
+                // There is a UNIQUE constraint on the QUERY column,
+                // so there should only be one match.
+                return null;
+            }
+            if (c.moveToFirst()) {
+                return ContentUris
+                    .withAppendedId(uri, c.getInt(c.getColumnIndex(SearchHistory._ID)));
+            }
+        } finally {
+            c.close();
+        }
+
+        /*
+         * SECOND: If the update failed, then insert a new record.
+         */
+        cv.put(SearchHistory.QUERY, query);
+        cv.put(SearchHistory.VISITS, 1);
+        cv.put(SearchHistory.DATE_LAST_VISITED, System.currentTimeMillis());
+
+        long id = db.insert(SearchHistory.TABLE_NAME, null, cv);
+
+        if (id < 0) {
+            return null;
+        }
+
+        return ContentUris.withAppendedId(uri, id);
+    }
+
+    @Override
+    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+        return getWritableDatabase(uri)
+                .delete(SearchHistory.TABLE_NAME, selection, selectionArgs);
+    }
+
+    /**
+     * Since we are managing counts and the full-text db, an update
+     * could mangle the internal state. So we disable it.
+     */
+    @Override
+    public int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException(
+                "This content provider does not support updating items");
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        String groupBy = null;
+        String having = null;
+        return getReadableDatabase(uri)
+                .query(SearchHistory.TABLE_NAME, projection, selection, selectionArgs,
+                        groupBy, having, sortOrder);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return SearchHistory.CONTENT_TYPE;
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -145,16 +145,17 @@ gbjar.sources += [
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
+    'db/SearchHistoryProvider.java',
     'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/SuggestedSites.java',
     'db/TabsProvider.java',
     'db/TopSitesCursorWrapper.java',
     'distribution/Distribution.java',
     'DoorHangerPopup.java',
     'DynamicToolbar.java',
--- a/mobile/android/base/preferences/GeckoPreferenceFragment.java
+++ b/mobile/android/base/preferences/GeckoPreferenceFragment.java
@@ -117,17 +117,19 @@ public class GeckoPreferenceFragment ext
             return;
         }
 
         Log.v(LOGTAG, "Setting activity title to " + newTitle);
         activity.setTitle(newTitle);
 
         if (Build.VERSION.SDK_INT >= 14) {
             final ActionBar actionBar = activity.getActionBar();
-            actionBar.setTitle(newTitle);
+            if (actionBar != null) {
+                actionBar.setTitle(newTitle);
+            }
         }
     }
 
     @Override
     public void onResume() {
         // This is a little delicate. Ensure that you do nothing prior to
         // super.onResume that you wouldn't do in onCreate.
         applyLocale(Locale.getDefault());
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -135,17 +135,19 @@ OnSharedPreferenceChangeListener
 
     private void updateActionBarTitle(int title) {
         if (Build.VERSION.SDK_INT >= 14) {
             final String newTitle = getString(title);
             if (newTitle != null) {
                 Log.v(LOGTAG, "Setting action bar title to " + newTitle);
 
                 final ActionBar actionBar = getActionBar();
-                actionBar.setTitle(newTitle);
+                if (actionBar != null) {
+                    actionBar.setTitle(newTitle);
+                }
             }
         }
     }
 
     private void updateTitle(String newTitle) {
         if (newTitle != null) {
             Log.v(LOGTAG, "Setting activity title to " + newTitle);
             setTitle(newTitle);
@@ -355,17 +357,19 @@ OnSharedPreferenceChangeListener
                     return longClickListener.onLongClick(view);
                 }
                 return false;
             }
         });
 
         if (Build.VERSION.SDK_INT >= 14) {
             final ActionBar actionBar = getActionBar();
-            actionBar.setHomeButtonEnabled(true);
+            if (actionBar != null) {
+                actionBar.setHomeButtonEnabled(true);
+            }
         }
 
         // N.B., if we ever need to redisplay the locale selection UI without
         // just finishing and recreating the activity, right here we'll need to
         // capture EXTRA_SHOW_FRAGMENT_TITLE from the intent and store the title ID.
 
         // If launched from notification, explicitly cancel the notification.
         if (intentExtras != null && intentExtras.containsKey(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION)) {
@@ -972,25 +976,25 @@ OnSharedPreferenceChangeListener
             return onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale), (String) newValue);
         }
 
         if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) {
             setCharEncodingState(((String) newValue).equals("true"));
         } else if (PREFS_ANNOUNCEMENTS_ENABLED.equals(prefName)) {
             // Send a broadcast intent to the product announcements service, either to start or
             // to stop the repeated background checks.
-            broadcastAnnouncementsPref(GeckoAppShell.getContext(), ((Boolean) newValue).booleanValue());
+            broadcastAnnouncementsPref(this, ((Boolean) newValue).booleanValue());
         } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) {
-            org.mozilla.gecko.updater.UpdateServiceHelper.registerForUpdates(GeckoAppShell.getContext(), (String) newValue);
+            org.mozilla.gecko.updater.UpdateServiceHelper.registerForUpdates(this, (String) newValue);
         } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) {
             // The healthreport pref only lives in Android, so we do not persist
             // to Gecko, but we do broadcast intent to the health report
             // background uploader service, which will start or stop the
             // repeated background upload attempts.
-            broadcastHealthReportUploadPref(GeckoAppShell.getContext(), ((Boolean) newValue).booleanValue());
+            broadcastHealthReportUploadPref(this, ((Boolean) newValue).booleanValue());
         } else if (PREFS_GEO_REPORTING.equals(prefName)) {
             // Translate boolean value to int for geo reporting pref.
             newValue = ((Boolean) newValue) ? 1 : 0;
         }
 
         // Send Gecko-side pref changes to Gecko
         if (isGeckoPref(prefName)) {
             PrefsHelper.setPref(prefName, newValue);
@@ -1008,17 +1012,17 @@ OnSharedPreferenceChangeListener
             final FontSizePreference fontSizePref = (FontSizePreference) preference;
             fontSizePref.setSummary(fontSizePref.getSavedFontSizeName());
         }
 
         return true;
     }
 
     private EditText getTextBox(int aHintText) {
-        EditText input = new FloatingHintEditText(GeckoAppShell.getContext());
+        EditText input = new FloatingHintEditText(this);
         int inputtype = InputType.TYPE_CLASS_TEXT;
         inputtype |= InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
         input.setInputType(inputtype);
 
         input.setHint(aHintText);
         return input;
     }
 
--- a/mobile/android/base/tests/helpers/WaitHelper.java
+++ b/mobile/android/base/tests/helpers/WaitHelper.java
@@ -22,17 +22,17 @@ import com.jayway.android.robotium.solo.
 /**
  * Provides functionality related to waiting on certain events to happen.
  */
 public final class WaitHelper {
     // TODO: Make public for when Solo.waitForCondition is used directly (i.e. do not want
     // assertion from waitFor)?
     private static final int DEFAULT_MAX_WAIT_MS = 5000;
     private static final int PAGE_LOAD_WAIT_MS = 10000;
-    private static final int CHANGE_WAIT_MS = 10000;
+    private static final int CHANGE_WAIT_MS = 15000;
 
     // TODO: via lucasr - Add ThrobberVisibilityChangeVerifier?
     private static final ChangeVerifier[] PAGE_LOAD_VERIFIERS = new ChangeVerifier[] {
         new ToolbarTitleTextChangeVerifier()
     };
 
     private static UITestContext sContext;
     private static Solo sSolo;
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -51,17 +51,17 @@ skip-if = processor == "x86"
 skip-if = android_version == "10" || processor == "x86"
 [testInputUrlBar]
 [testJarReader]
 [testLinkContextMenu]
 # [testHomeListsProvider] # see bug 952310
 [testHomeProvider]
 [testLoad]
 [testMailToContextMenu]
-[testMasterPassword]
+# [testMasterPassword] disabled for being finicky, see bug 1033013
 # disabled on 2.3; bug 979603
 # disabled on 4.0; bug 1006242
 skip-if = android_version == "10" || android_version == "15"
 [testNewTab]
 # disabled on 2.3; bug 995696
 skip-if = android_version == "10"
 [testOverscroll]
 # disabled on 2.3; bug 836818
@@ -75,16 +75,17 @@ skip-if = processor == "x86"
 [testPictureLinkContextMenu]
 [testPrefsObserver]
 [testPrivateBrowsing]
 [testPromptGridInput]
 # disabled on x86 only; bug 957185
 skip-if = processor == "x86"
 # [testReaderMode] # see bug 913254, 936224
 [testReadingListProvider]
+[testSearchHistoryProvider]
 [testSearchSuggestions]
 # disabled on x86; bug 907768
 skip-if = processor == "x86"
 [testSessionOOMSave]
 # disabled on x86 and 2.3; bug 945395
 skip-if = android_version == "10" || processor == "x86"
 [testSessionOOMRestore]
 # disabled on Android 2.3; bug 979600
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testSearchHistoryProvider.java
@@ -0,0 +1,269 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tests;
+
+import java.util.concurrent.Callable;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.SearchHistory;
+import org.mozilla.gecko.db.SearchHistoryProvider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class testSearchHistoryProvider extends ContentProviderTest {
+
+    // Translations of "United Kingdom" in several different languages
+    private static final String[] testStrings = {"An Ríocht Aontaithe", // Irish
+            "Angli", // Albanian
+            "Britanniarum Regnum", // Latin
+            "Britio", // Esperanto
+            "Büyük Britanya", // Turkish
+            "Egyesült Királyság", // Hungarian
+            "Erresuma Batua", // Basque
+            "Inggris Raya", // Indonesian
+            "Ir-Renju Unit", // Maltese
+            "Iso-Britannia", // Finnish
+            "Jungtinė Karalystė", // Lithuanian
+            "Lielbritānija", // Latvian
+            "Regatul Unit", // Romanian
+            "Regne Unit", // Catalan, Valencian
+            "Regno Unito", // Italian
+            "Royaume-Uni", // French
+            "Spojené království", // Czech
+            "Spojené kráľovstvo", // Slovak
+            "Storbritannia", // Norwegian
+            "Storbritannien", // Danish
+            "Suurbritannia", // Estonian
+            "Ujedinjeno Kraljevstvo", // Bosnian
+            "United Alaeze", // Igbo
+            "United Kingdom", // English
+            "Vereinigtes Königreich", // German
+            "Verenigd Koninkrijk", // Dutch
+            "Verenigde Koninkryk", // Afrikaans
+            "Vương quốc Anh", // Vietnamese
+            "Wayòm Ini", // Haitian, Haitian Creole
+            "Y Deyrnas Unedig", // Welsh
+            "Združeno kraljestvo", // Slovene
+            "Zjednoczone Królestwo", // Polish
+            "Ηνωμένο Βασίλειο", // Greek (modern)
+            "Великобритания", // Russian
+            "Нэгдсэн Вант Улс", // Mongolian
+            "Обединетото Кралство", // Macedonian
+            "Уједињено Краљевство", // Serbian
+            "Միացյալ Թագավորություն", // Armenian
+            "בריטניה", // Hebrew (modern)
+            "פֿאַראייניקטע מלכות", // Yiddish
+            "المملكة المتحدة", // Arabic
+            "برطانیہ", // Urdu
+            "پادشاهی متحده", // Persian (Farsi)
+            "यूनाइटेड किंगडम", // Hindi
+            "संयुक्त राज्य", // Nepali
+            "যুক্তরাজ্য", // Bengali, Bangla
+            "યુનાઇટેડ કિંગડમ", // Gujarati
+            "ஐக்கிய ராஜ்யம்", // Tamil
+            "สหราชอาณาจักร", // Thai
+            "ສະ​ຫະ​ປະ​ຊາ​ຊະ​ອາ​ນາ​ຈັກ", // Lao
+            "გაერთიანებული სამეფო", // Georgian
+            "イギリス", // Japanese
+            "联合王国" // Chinese
+    };
+
+
+    private static final String DB_NAME = "searchhistory.db";
+
+    /**
+     * Boilerplate alert.
+     * <p/>
+     * Make sure this method is present and that it returns a new
+     * instance of your class.
+     */
+    private static Callable<ContentProvider> sProviderFactory =
+            new Callable<ContentProvider>() {
+                @Override
+                public ContentProvider call() {
+                    return new SearchHistoryProvider();
+                }
+            };
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp(sProviderFactory, BrowserContract.SEARCH_HISTORY_AUTHORITY, DB_NAME);
+        mTests.add(new TestInsert());
+        mTests.add(new TestUnicodeQuery());
+        mTests.add(new TestTimestamp());
+        mTests.add(new TestDelete());
+        mTests.add(new TestIncrement());
+    }
+
+    public void testSearchHistory() throws Exception {
+        for (Runnable test : mTests) {
+            String testName = test.getClass().getSimpleName();
+            setTestName(testName);
+            mAsserter.dumpLog(
+                    "testBrowserProvider: Database empty - Starting " + testName + ".");
+            // Clear the db
+            mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            test.run();
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestInsert extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv;
+            for (int i = 0; i < testStrings.length; i++) {
+                cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            }
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), testStrings.length,
+                    "Should have one row for each insert");
+
+            c.close();
+        }
+    }
+
+    /**
+     * Verify that we can insert values into the DB, including unicode.
+     */
+    private class TestUnicodeQuery extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv;
+            Cursor c = null;
+            String selection = SearchHistory.QUERY + " = ?";
+
+            for (int i = 0; i < testStrings.length; i++) {
+                cv = new ContentValues();
+                cv.put(SearchHistory.QUERY, testStrings[i]);
+                mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+                c = mProvider.query(SearchHistory.CONTENT_URI, null, selection,
+                        new String[]{ testStrings[i] }, null);
+                mAsserter.is(c.getCount(), 1,
+                        "Should have one row for insert of " + testStrings[i]);
+            }
+
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    /**
+     * Verify that timestamps are updated on insert.
+     */
+    private class TestTimestamp extends TestCase {
+        @Override
+        public void test() throws Exception {
+            String insertedTerm = "Courtside Seats";
+            long insertStart;
+            long insertFinish;
+            long t1Db;
+            long t2Db;
+
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+
+            // First check that the DB has a value that is close to the
+            // system time.
+            insertStart = System.currentTimeMillis();
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            t1Db = c.getLong(c.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            c.close();
+            insertFinish = System.currentTimeMillis();
+            mAsserter.ok(t1Db >= insertStart, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+            mAsserter.ok(t1Db <= insertFinish, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+
+            insertStart = System.currentTimeMillis();
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            t2Db = c.getLong(c.getColumnIndex(SearchHistory.DATE_LAST_VISITED));
+            c.close();
+            insertFinish = System.currentTimeMillis();
+
+            mAsserter.ok(t2Db >= insertStart, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+            mAsserter.ok(t2Db <= insertFinish, "DATE_LAST_VISITED",
+                    "Date last visited should be set on insert.");
+            mAsserter.ok(t2Db > t1Db, "DATE_LAST_VISITED",
+                    "Date last visited should be updated on key increment.");
+        }
+    }
+
+    /**
+     * Verify that sending a delete command empties the database.
+     */
+    private class TestDelete extends TestCase {
+        @Override
+        public void test() throws Exception {
+            String insertedTerm = "Courtside Seats";
+
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, insertedTerm);
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 1, "Should have one value");
+            mProvider.delete(SearchHistory.CONTENT_URI, null, null);
+            c.close();
+
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 0, "Should be empty");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c.close();
+
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 1, "Should have one value");
+            c.close();
+        }
+    }
+
+
+    /**
+     * Ensure that we only increment when the case matches.
+     */
+    private class TestIncrement extends TestCase {
+        @Override
+        public void test() throws Exception {
+            ContentValues cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+
+            Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            c.moveToFirst();
+            mAsserter.is(c.getCount(), 1, "Should have one result");
+            mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2,
+                    "Counter should be 2");
+            c.close();
+
+            cv = new ContentValues();
+            cv.put(SearchHistory.QUERY, "Omaha");
+            mProvider.insert(SearchHistory.CONTENT_URI, cv);
+            c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null);
+            mAsserter.is(c.getCount(), 2, "Should have two results");
+            c.close();
+        }
+    }
+}
--- a/mobile/locales/en-US/searchplugins/yahoo.xml
+++ b/mobile/locales/en-US/searchplugins/yahoo.xml
@@ -11,12 +11,12 @@
   <Param name="output" value="fxjson" />
   <Param name="appid" value="ffm" />
   <Param name="command" value="{searchTerms}" />
   <MozParam name="nresults" condition="pref" pref="maxSuggestions" />
 </Url>
 <Url type="text/html" method="GET" template="https://search.yahoo.com/search">
   <Param name="p" value="{searchTerms}" />
   <Param name="ei" value="UTF-8" />
-  <Param name="fr" value="mozilla_mobile_search" />
+  <Param name="fr" value="mozmob1" />
 </Url>
 <SearchForm>https://search.yahoo.com/</SearchForm>
 </SearchPlugin>
--- a/services/fxaccounts/FxAccountsManager.jsm
+++ b/services/fxaccounts/FxAccountsManager.jsm
@@ -133,16 +133,25 @@ this.FxAccountsManager = {
             user: user
           });
         }
 
         // If the user object includes an email field, it may differ in
         // capitalization from what we sent down.  This is the server's
         // canonical capitalization and should be used instead.
         user.email = user.email || aEmail;
+
+        // If we're using server-side sign to refreshAuthentication
+        // we don't need to update local state; also because of two
+        // interacting glitches we need to bypass an event emission.
+        // See https://bugzilla.mozilla.org/show_bug.cgi?id=1031580
+        if (this._refreshing) {
+          return Promise.resolve({user: this._user});
+        }
+
         return this._fxAccounts.setSignedInUser(user).then(
           () => {
             this._activeSession = user;
             log.debug("User signed in: " + JSON.stringify(this._user) +
                       " - Account created " + (aMethod == "signUp"));
             return Promise.resolve({
               accountCreated: aMethod === "signUp",
               user: this._user
--- a/testing/xpcshell/xpcshell_b2g.ini
+++ b/testing/xpcshell/xpcshell_b2g.ini
@@ -1,15 +1,16 @@
 ; This Source Code Form is subject to the terms of the Mozilla Public
 ; License, v. 2.0. If a copy of the MPL was not distributed with this
 ; file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 [include:dom/apps/tests/unit/xpcshell.ini]
 [include:dom/mobilemessage/tests/xpcshell/xpcshell.ini]
 [include:dom/network/tests/unit_stats/xpcshell.ini]
+[include:dom/resourcestats/tests/xpcshell/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]
--- a/toolkit/devtools/discovery/discovery.js
+++ b/toolkit/devtools/discovery/discovery.js
@@ -324,16 +324,19 @@ Discovery.prototype = {
   },
 
   _onRemoteUpdate: function(e, update) {
     log("GOT REMOTE UPDATE");
 
     let remoteDevice = update.device;
     let remoteHost = update.from;
 
+    // Record the reply as received so it won't be purged as missing
+    this._expectingReplies.from.delete(remoteDevice);
+
     // First, loop over the known services
     for (let service in this.remoteServices) {
       let devicesWithService = this.remoteServices[service];
       let hadServiceForDevice = !!devicesWithService[remoteDevice];
       let haveServiceForDevice = service in update.services;
       // If the remote device used to have service, but doesn't any longer, then
       // it was deleted, so we remove it here.
       if (hadServiceForDevice && !haveServiceForDevice) {
--- a/toolkit/devtools/discovery/tests/unit/test_discovery.js
+++ b/toolkit/devtools/discovery/tests/unit/test_discovery.js
@@ -111,16 +111,19 @@ add_task(function*() {
             { tux: true,  host: "localhost" });
 
   discovery.removeService("devtools");
   yield scanForChange("devtools", "removed");
 
   discovery.addService("penguins", { tux: false });
   yield scanForChange("penguins", "updated");
 
+  // Scan again, but nothing should be removed
+  yield scanForNoChange("penguins", "removed");
+
   // Split the scanning side from the service side to simulate the machine with
   // the service becoming unreachable
   gTestTransports = {};
 
   discovery.removeService("penguins");
   yield scanForChange("penguins", "removed");
 });
 
@@ -132,8 +135,22 @@ function scanForChange(service, changeTy
   discovery.on(service + "-device-" + changeType, function onChange() {
     discovery.off(service + "-device-" + changeType, onChange);
     clearTimeout(timer);
     deferred.resolve();
   });
   discovery.scan();
   return deferred.promise;
 }
+
+function scanForNoChange(service, changeType) {
+  let deferred = promise.defer();
+  let timer = setTimeout(() => {
+    deferred.resolve();
+  }, discovery.replyTimeout + 500);
+  discovery.on(service + "-device-" + changeType, function onChange() {
+    discovery.off(service + "-device-" + changeType, onChange);
+    clearTimeout(timer);
+    deferred.reject(new Error("Unexpected change occurred"));
+  });
+  discovery.scan();
+  return deferred.promise;
+}
--- a/toolkit/devtools/server/actors/call-watcher.js
+++ b/toolkit/devtools/server/actors/call-watcher.js
@@ -217,17 +217,17 @@ let FunctionCallActor = protocol.ActorCl
         return "Function";
       }
       if (typeof arg == "object") {
         return "Object";
       }
       // If this argument matches the method's signature
       // and is an enum, change it to its constant name.
       if (enumArgs && enumArgs.indexOf(i) !== -1) {
-        return getEnumsLookupTable(global, caller)[arg] || arg;
+        return getBitToEnumValue(global, caller, arg);
       }
       return arg;
     });
 
     return serializeArgs().join(", ");
   }
 });
 
@@ -636,24 +636,53 @@ CallWatcherFront.ENUM_METHODS[CallWatche
  * assuming they look LIKE_THIS most of the time.
  *
  * For example, when gl.clear(gl.COLOR_BUFFER_BIT) is called, the actual passed
  * argument's value is 16384, which we want identified as "COLOR_BUFFER_BIT".
  */
 var gEnumRegex = /^[A-Z_]+$/;
 var gEnumsLookupTable = {};
 
-function getEnumsLookupTable(type, object) {
-  let cachedEnum = gEnumsLookupTable[type];
-  if (cachedEnum) {
-    return cachedEnum;
-  }
+// These values are returned from errors, or empty values,
+// and need to be ignored when checking arguments due to the bitwise math.
+var INVALID_ENUMS = [
+  "INVALID_ENUM", "NO_ERROR", "INVALID_VALUE", "OUT_OF_MEMORY", "NONE"
+];
+
+function getBitToEnumValue(type, object, arg) {
+  let table = gEnumsLookupTable[type];
 
-  let table = gEnumsLookupTable[type] = {};
+  // If mapping not yet created, do it on the first run.
+  if (!table) {
+    table = gEnumsLookupTable[type] = {};
 
-  for (let key in object) {
-    if (key.match(gEnumRegex)) {
-      table[object[key]] = key;
+    for (let key in object) {
+      if (key.match(gEnumRegex)) {
+        // Maps `16384` to `"COLOR_BUFFER_BIT"`, etc.
+        table[object[key]] = key;
+      }
     }
   }
 
-  return table;
+  // If a single bit value, just return it.
+  if (table[arg]) {
+    return table[arg];
+  }
+
+  // Otherwise, attempt to reduce it to the original bit flags:
+  // `16640` -> "COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT"
+  let flags = [];
+  for (let flag in table) {
+    if (INVALID_ENUMS.indexOf(table[flag]) !== -1) {
+      continue;
+    }
+
+    // Cast to integer as all values are stored as strings
+    // in `table`
+    flag = flag | 0;
+    if (flag && (arg & flag) === flag) {
+      flags.push(table[flag]);
+    }
+  }
+
+  // Cache the combined bitmask value
+  return table[arg] = flags.join(" | ") || arg;
 }
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -896,25 +896,31 @@ const TAB_CHARS = "\t";
 
 /**
  * Prettify minified CSS text.
  * This prettifies CSS code where there is no indentation in usual places while
  * keeping original indentation as-is elsewhere.
  * @param string text The CSS source to prettify.
  * @return string Prettified CSS source
  */
-CssLogic.prettifyCSS = function(text) {
+CssLogic.prettifyCSS = function(text, ruleCount) {
   if (CssLogic.LINE_SEPARATOR == null) {
     let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
     CssLogic.LINE_SEPARATOR = (os === "WINNT" ? "\r\n" : "\n");
   }
 
   // remove initial and terminating HTML comments and surrounding whitespace
   text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, "");
 
+  // don't attempt to prettify if there's more than one line per rule.
+  let lineCount = text.split("\n").length - 1;
+  if (ruleCount !== null && lineCount >= ruleCount) {
+    return text;
+  }
+
   let parts = [];    // indented parts
   let partStart = 0; // start offset of currently parsed part
   let indent = "";
   let indentLevel = 0;
 
   for (let i = 0; i < text.length; i++) {
     let c = text[i];
     let shouldIndent = false;
--- a/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul
@@ -18,25 +18,29 @@
 
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_ERROR_PATCHING,
   buttonClick: "next"
 }, {
-  pageid: PAGEID_DOWNLOADING
+  pageid: PAGEID_DOWNLOADING,
+  extraStartFunction: createContinueFile
 }, {
   pageid: PAGEID_FINISHED,
-  buttonClick: "extra1"
+  buttonClick: "extra1",
+  extraStartFunction: removeContinueFile
 } ];
 
 function runTest() {
   debugDump("entering");
 
+  removeContinueFile();
+
   // Specify the url to update.sjs with a slowDownloadMar param so the ui can
   // load before the download completes.
   let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
   let patches = getLocalPatchString("partial", null, null, null, null, null,
                                     STATE_PENDING) +
                 getLocalPatchString("complete", slowDownloadURL, null, null,
                                     null, "false");
   let updates = getLocalUpdateString(patches, null, null, null,
--- a/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
@@ -11,32 +11,37 @@
 <window title="Update Wizard pages: error patching, download, and errors (partial failed and download complete verification failure)"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="runTestDefault();">
 <script type="application/javascript"
         src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
 <script type="application/javascript"
         src="utils.js"/>
 
+<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_ERROR_PATCHING,
   buttonClick: "next"
 }, {
-  pageid: PAGEID_DOWNLOADING
+  pageid: PAGEID_DOWNLOADING,
+  extraStartFunction: createContinueFile
 }, {
   pageid: PAGEID_ERRORS,
-  buttonClick: "finish"
+  buttonClick: "finish",
+  extraStartFunction: removeContinueFile
 } ];
 
 function runTest() {
   debugDump("entering");
 
+  removeContinueFile();
+
   // Specify the url to update.sjs with a slowDownloadMar param so the ui can
   // load before the download completes.
   let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
   let patches = getLocalPatchString("partial", null, null, null, null, null,
                                     STATE_PENDING) +
                 getLocalPatchString("complete", slowDownloadURL, "MD5",
                                     "1234cd43a1c77e30191c53a329a3f99d", null,
                                     "false");
--- a/toolkit/mozapps/update/tests/chrome/update.sjs
+++ b/toolkit/mozapps/update/tests/chrome/update.sjs
@@ -39,33 +39,47 @@ function handleRequest(aRequest, aRespon
   }
 
   // When a mar download is started by the update service it can finish
   // downloading before the ui has loaded. By specifying a serviceURL for the
   // update patch that points to this file and has a slowDownloadMar param the
   // mar will be downloaded asynchronously which will allow the ui to load
   // before the download completes.
   if (params.slowDownloadMar) {
+    var i;
     aResponse.processAsync();
     aResponse.setHeader("Content-Type", "binary/octet-stream");
     aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
+    var continueFile = AUS_Cc["@mozilla.org/file/directory_service;1"].
+                       getService(AUS_Ci.nsIProperties).
+                       get("CurWorkD", AUS_Ci.nsILocalFile);
+    var continuePath = REL_PATH_DATA + "continue";
+    var continuePathParts = continuePath.split("/");
+    for (i = 0; i < continuePathParts.length; ++i) {
+      continueFile.append(continuePathParts[i]);
+    }
+
     var marFile = AUS_Cc["@mozilla.org/file/directory_service;1"].
                   getService(AUS_Ci.nsIProperties).
                   get("CurWorkD", AUS_Ci.nsILocalFile);
     var path = REL_PATH_DATA + FILE_SIMPLE_MAR;
     var pathParts = path.split("/");
-    for(var i = 0; i < pathParts.length; ++i)
+    for (i = 0; i < pathParts.length; ++i) {
       marFile.append(pathParts[i]);
+    }
     var contents = readFileBytes(marFile);
     gTimer = AUS_Cc["@mozilla.org/timer;1"].
              createInstance(AUS_Ci.nsITimer);
     gTimer.initWithCallback(function(aTimer) {
-      aResponse.write(contents);
-      aResponse.finish();
-    }, SLOW_MAR_DOWNLOAD_INTERVAL, AUS_Ci.nsITimer.TYPE_ONE_SHOT);
+      if (continueFile.exists()) {
+        gTimer.cancel();
+        aResponse.write(contents);
+        aResponse.finish();
+      }
+    }, SLOW_MAR_DOWNLOAD_INTERVAL, AUS_Ci.nsITimer.TYPE_REPEATING_SLACK);
     return;
   }
 
   if (params.uiURL) {
     var remoteType = "";
     if (!params.remoteNoTypeAttr &&
         (params.uiURL == "BILLBOARD" || params.uiURL == "LICENSE")) {
       remoteType = " " + params.uiURL.toLowerCase() + "=\"1\"";
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -478,29 +478,68 @@ function delayedDefaultCallback() {
     gTest.extraDelayedCheckFunction();
   }
 
   // Used to verify that this test has been performed
   gTest.ranTest = true;
 
   if (gTest.buttonClick) {
     debugDump("clicking " + gTest.buttonClick + " button");
-    if(gTest.extraDelayedFinishFunction) {
+    if (gTest.extraDelayedFinishFunction) {
       throw("Tests cannot have a buttonClick and an extraDelayedFinishFunction property");
     }
     gDocElem.getButton(gTest.buttonClick).click();
   }
   else if (gTest.extraDelayedFinishFunction) {
     debugDump("calling extraDelayedFinishFunction " +
               gTest.extraDelayedFinishFunction.name);
     gTest.extraDelayedFinishFunction();
   }
 }
 
 /**
+ * Gets the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests without creating it.
+ *
+ * @return nsILocalFile for the continue file.
+ */
+function getContinueFile() {
+  let continueFile = AUS_Cc["@mozilla.org/file/directory_service;1"].
+                     getService(AUS_Ci.nsIProperties).
+                     get("CurWorkD", AUS_Ci.nsILocalFile);
+  let continuePath = REL_PATH_DATA + "/continue";
+  let continuePathParts = continuePath.split("/");
+  for (let i = 0; i < continuePathParts.length; ++i) {
+    continueFile.append(continuePathParts[i]);
+  }
+  return continueFile;
+}
+
+/**
+ * Creates the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests.
+ */
+function createContinueFile() {
+  debugDump("creating 'continue' file for slow mar downloads");
+  writeFile(getContinueFile(), "");
+}
+
+/**
+ * Removes the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests.
+ */
+function removeContinueFile() {
+  let continueFile = getContinueFile();
+  if (continueFile.exists()) {
+    debugDump("removing 'continue' file for slow mar downloads");
+    continueFile.remove(false);
+  }
+}
+
+/**
  * Checks the wizard page buttons' disabled and hidden attributes values are
  * correct. If an expected button id is not specified then the expected disabled
  * and hidden attribute value is true.
  */
 function checkButtonStates() {
   debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid);
 
   const buttonNames = ["extra1", "extra2", "back", "next", "finish", "cancel"];
--- a/widget/gonk/nsAppShell.cpp
+++ b/widget/gonk/nsAppShell.cpp
@@ -57,17 +57,16 @@
 #include "nsWindow.h"
 #include "OrientationObserver.h"
 #include "GonkMemoryPressureMonitoring.h"
 
 #include "android/log.h"
 #include "libui/EventHub.h"
 #include "libui/InputReader.h"
 #include "libui/InputDispatcher.h"
-#include "cutils/properties.h"
 
 #ifdef MOZ_NUWA_PROCESS
 #include "ipc/Nuwa.h"
 #endif
 
 #include "mozilla/Preferences.h"
 #include "GeckoProfiler.h"
 
@@ -1047,19 +1046,17 @@ nsAppShell::Exit()
         obsServ->RemoveObserver(this, "network-connection-state-changed");
     }
     return nsBaseAppShell::Exit();
 }
 
 void
 nsAppShell::InitInputDevices()
 {
-    char value[PROPERTY_VALUE_MAX];
-    property_get("ro.moz.devinputjack", value, "0");
-    sDevInputAudioJack = !strcmp(value, "1");
+    sDevInputAudioJack = hal::IsHeadphoneEventFromInputDev();
     sHeadphoneState = AKEY_STATE_UNKNOWN;
     sMicrophoneState = AKEY_STATE_UNKNOWN;
 
     mEventHub = new EventHub();
     mReaderPolicy = new GeckoInputReaderPolicy();
     mReaderPolicy->setDisplayInfo();
     mDispatcher = new GeckoInputDispatcher(mEventHub);