Merge m-c to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 24 Apr 2014 13:20:37 +0200
changeset 179959 fb7439e1e112b9320dfe7317b133f0a98a072e1c
parent 179958 3dd6d30c3050fbf931c6a2f98333b66f2fd55faf (current diff)
parent 179879 9d3da41ad0b6ef945bd06c680a9e23fcfc1fa406 (diff)
child 179960 5b54b4287fbf08ae3801de0b42c1c26067852da2
push idunknown
push userunknown
push dateunknown
milestone31.0a1
Merge m-c to mozilla-inbound
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="0292e64ef8451df104dcf9ac3b2c6749b81684dd"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="85f9690323b235f4dcf2901ea2240d3c60fc22a0"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e6383e6e785cc3ea237e902beb1092f9aa88e29d">
     <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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <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"/>
@@ -123,14 +123,14 @@
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="09485b73629856b21b2ed6073e327ab0e69a1189"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
-  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9a00b3db898a83ef0766baa93e935e525572899e"/>
+  <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="bb11a0417efa7e6a08ed1cb2d5d6a13a3100abd3"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="72e3a520e3c700839f07ba0113fd527b923c3330"/>
   <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="baaf899afb158b9530690002f3656e958e3eb047"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/>
 </manifest>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="65fba428f8d76336b33ddd9e15900357953600ba">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <!-- Stock Android things -->
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="0292e64ef8451df104dcf9ac3b2c6749b81684dd"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="85f9690323b235f4dcf2901ea2240d3c60fc22a0"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -13,17 +13,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e6383e6e785cc3ea237e902beb1092f9aa88e29d">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "9f480d1c52a46cac8e9a0be04bdb0d73810bc596", 
+    "revision": "2eae294604eb70e0e6eaee76b17e155ffd031107", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e6383e6e785cc3ea237e902beb1092f9aa88e29d">
     <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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="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="d25852a189c9707b144eb5f82d08384eb066c0fd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="6a2eb4b3664fb3e6f5c87db2af8485b7424f9ecb"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8d6c36d74ba9aefbc8c3618fc93dd4907a0dbf5e"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/base/content/browser-customization.js
+++ b/browser/base/content/browser-customization.js
@@ -34,17 +34,16 @@ let CustomizationHandler = {
       childNode.setAttribute("disabled", true);
 
     let cmd = document.getElementById("cmd_CustomizeToolbars");
     cmd.setAttribute("disabled", "true");
 
     UpdateUrlbarSearchSplitterState();
 
     CombinedStopReload.uninit();
-    CombinedBackForward.uninit();
     PlacesToolbarHelper.customizeStart();
     DownloadsButton.customizeStart();
 
     // The additional padding on the sides of the browser
     // can cause the customize tab to get clipped.
     let tabContainer = gBrowser.tabContainer;
     if (tabContainer.getAttribute("overflow") == "true") {
       let tabstrip = tabContainer.mTabstrip;
@@ -82,17 +81,16 @@ let CustomizationHandler = {
     }
 
     PlacesToolbarHelper.customizeDone();
     DownloadsButton.customizeDone();
 
     // The url bar splitter state is dependent on whether stop/reload
     // and the location bar are combined, so we need this ordering
     CombinedStopReload.init();
-    CombinedBackForward.init();
     UpdateUrlbarSearchSplitterState();
 
     // Update the urlbar
     if (gURLBar) {
       URLBarSetURI();
       XULBrowserWindow.asyncUpdateUI();
     }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -255,25 +255,17 @@ function UpdateBackForwardCommands(aWebN
   var forwardDisabled = forwardBroadcaster.hasAttribute("disabled");
   if (backDisabled == aWebNavigation.canGoBack) {
     if (backDisabled)
       backBroadcaster.removeAttribute("disabled");
     else
       backBroadcaster.setAttribute("disabled", true);
   }
 
-  let canGoForward = aWebNavigation.canGoForward;
-  if (forwardDisabled) {
-    // Force the button to either be hidden (if we are already disabled,
-    // and should be), or to show if we're about to un-disable it:
-    // otherwise no transition will occur and it'll never show:
-    CombinedBackForward.setForwardButtonOcclusion(!canGoForward);
-  }
-
-  if (forwardDisabled == canGoForward) {
+  if (forwardDisabled == aWebNavigation.canGoForward) {
     if (forwardDisabled)
       forwardBroadcaster.removeAttribute("disabled");
     else
       forwardBroadcaster.setAttribute("disabled", true);
   }
 }
 
 /**
@@ -915,17 +907,16 @@ var gBrowserInit = {
         gURLBar.setAttribute("readonly", "true");
         gURLBar.setAttribute("enablehistory", "false");
       }
       goSetCommandEnabled("cmd_newNavigatorTab", false);
     }
 
     // Misc. inits.
     CombinedStopReload.init();
-    CombinedBackForward.init();
     gPrivateBrowsingUI.init();
     TabsInTitlebar.init();
 
     // Wait until chrome is painted before executing code not critical to making the window visible
     this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar);
     window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
 
     this._loadHandled = true;
@@ -1262,17 +1253,16 @@ var gBrowserInit = {
     if (desc && !desc.get) {
       DeveloperToolbar.destroy();
     }
 
     // First clean up services initialized in gBrowserInit.onLoad (or those whose
     // uninit methods don't depend on the services having been initialized).
 
     CombinedStopReload.uninit();
-    CombinedBackForward.uninit();
 
     gGestureSupport.init(false);
 
     gHistorySwipeAnimation.uninit();
 
     FullScreen.cleanup();
 
 #ifdef MOZ_SERVICES_SYNC
@@ -3794,17 +3784,16 @@ var XULBrowserWindow = {
     } catch (e) {}
     gIdentityHandler.checkIdentity(this._state, uri);
   },
 
   // simulate all change notifications after switching tabs
   onUpdateCurrentBrowser: function XWB_onUpdateCurrentBrowser(aStateFlags, aStatus, aMessage, aTotalProgress) {
     if (FullZoom.updateBackgroundTabs)
       FullZoom.onLocationChange(gBrowser.currentURI, true);
-    CombinedBackForward.setForwardButtonOcclusion(!gBrowser.webProgress.canGoForward);
     var nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
     var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP;
     // use a pseudo-object instead of a (potentially nonexistent) channel for getting
     // a correct error message - and make sure that the UI is always either in
     // loading (STATE_START) or done (STATE_STOP) mode
     this.onStateChange(
       gBrowser.webProgress,
       { URI: gBrowser.currentURI },
@@ -3869,53 +3858,16 @@ var LinkTargetDisplay = {
 
   _hide: function () {
     clearTimeout(this._timer);
 
     XULBrowserWindow.updateStatusField();
   }
 };
 
-let CombinedBackForward = {
-  init: function() {
-    this.forwardButton = document.getElementById("forward-button");
-    // Add a transition listener to the url bar to hide the forward button
-    // when necessary
-    if (gURLBar)
-      gURLBar.addEventListener("transitionend", this);
-    // On startup, or if the user customizes, our listener isn't attached,
-    // and no transitions fire anyway, so we need to make sure we've hidden the
-    // button if necessary:
-    if (this.forwardButton && this.forwardButton.hasAttribute("disabled")) {
-      this.setForwardButtonOcclusion(true);
-    }
-  },
-  uninit: function() {
-    if (gURLBar)
-      gURLBar.removeEventListener("transitionend", this);
-  },
-  handleEvent: function(aEvent) {
-    if (aEvent.type == "transitionend" &&
-        (aEvent.propertyName == "margin-left" || aEvent.propertyName == "margin-right") &&
-        this.forwardButton.hasAttribute("disabled")) {
-      this.setForwardButtonOcclusion(true);
-    }
-  },
-  setForwardButtonOcclusion: function(shouldBeOccluded) {
-    if (!this.forwardButton)
-      return;
-
-    let hasAttribute = this.forwardButton.hasAttribute("occluded-by-urlbar");
-    if (shouldBeOccluded && !hasAttribute)
-      this.forwardButton.setAttribute("occluded-by-urlbar", "true");
-    else if (!shouldBeOccluded && hasAttribute)
-      this.forwardButton.removeAttribute("occluded-by-urlbar");
-  }
-}
-
 var CombinedStopReload = {
   init: function () {
     if (this._initialized)
       return;
 
     let reload = document.getElementById("urlbar-reload-button");
     let stop = document.getElementById("urlbar-stop-button");
     if (!stop || !reload || reload.nextSibling != stop)
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -651,31 +651,24 @@
                      class="chromeclass-location" overflows="false">
           <toolbarbutton id="back-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                          label="&backCmd.label;"
                          command="Browser:BackOrBackDuplicate"
                          cui-areatype="toolbar"
                          onclick="checkForMiddleClick(this, event);"
                          tooltip="back-button-tooltip"
                          context="backForwardMenu"/>
-          <toolbarbutton id="forward-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
-                         label="&forwardCmd.label;"
-                         command="Browser:ForwardOrForwardDuplicate"
-                         cui-areatype="toolbar"
-                         onclick="checkForMiddleClick(this, event);"
-                         tooltip="forward-button-tooltip"
-                         context="backForwardMenu"/>
-          <dummyobservertarget hidden="true"
-                               onbroadcast="if (this.getAttribute('disabled') == 'true')
-                                              this.parentNode.setAttribute('forwarddisabled', 'true');
-                                            else
-                                              this.parentNode.removeAttribute('forwarddisabled');">
-            <observes element="Browser:ForwardOrForwardDuplicate" attribute="disabled"/>
-          </dummyobservertarget>
-          <hbox id="urlbar-wrapper" flex="1" align="center">
+          <hbox id="urlbar-wrapper" flex="1">
+            <toolbarbutton id="forward-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+                           label="&forwardCmd.label;"
+                           command="Browser:ForwardOrForwardDuplicate"
+                           cui-areatype="toolbar"
+                           onclick="checkForMiddleClick(this, event);"
+                           tooltip="forward-button-tooltip"
+                           context="backForwardMenu"/>
             <textbox id="urlbar" flex="1"
                      placeholder="&urlbar.placeholder2;"
                      type="autocomplete"
                      autocompletesearch="urlinline history"
                      autocompletesearchparam="enable-actions"
                      autocompletepopup="PopupAutoCompleteRichResult"
                      completeselectedindex="true"
                      tabscrolling="true"
@@ -1183,32 +1176,23 @@
                          oncommand="DeveloperToolbar.hide();"
                          tooltiptext="&devToolbarCloseButton.tooltiptext;"/>
 #endif
    </toolbar>
   </vbox>
 
   <svg:svg height="0">
 #include tab-shape.inc.svg
-
+    <svg:clipPath id="urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse">
 #ifndef XP_MACOSX
-    <svg:clipPath id="keyhole-forward-clip-path" clipPathUnits="objectBoundingBox">
-      <svg:path d="m 0,0 c .3,.25 .3,.75, 0,1 l 1,0 0,-1 z"/>
-    </svg:clipPath>
-    <svg:clipPath id="urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse">
-      <svg:path d="m 0,-5 l 0,7.8 c 2.5,3.2 4,6.2 4,10.2 c 0,4 -1.5,7 -4,10 l 0,22l10000,0 l 0,-50 l -10000,0 z"/>
+      <svg:path d="m 1,-5 l 0,7.8 c 2.5,3.2 4,6.2 4,10.2 c 0,4 -1.5,7 -4,10 l 0,22l10000,0 l 0,-50 l -10000,0 z"/>
+#else
+      <svg:path d="M -11,-5 a 16 16 0 0 1 0,34 l 10000,0 l 0,-34 l -10000,0 z"/>
+#endif
     </svg:clipPath>
-#else
-    <svg:clipPath id="osx-keyhole-forward-clip-path" clipPathUnits="userSpaceOnUse">
-      <svg:path d="M 0,0 a 16 16 0 0 1 0,24 l 10000,0 l 0,-24 l -10000,0 z"/>
-    </svg:clipPath>
-    <svg:clipPath id="osx-urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse">
-      <svg:path d="M -12,-5 a 16 16 0 0 1 0,34 l 10000,0 l 0,-34 l -10000,0 z"/>
-    </svg:clipPath>
-#endif
   </svg:svg>
 
 </vbox>
 # <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck.
 #     Introducing the iframe dynamically, as needed, was found to be better than
 #     starting with an empty iframe here in browser.xul from a Ts standpoint.
 </deck>
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1007,22 +1007,22 @@
 
             newBrowser.setAttribute("type", "content-primary");
             newBrowser.docShellIsActive =
               (window.windowState != window.STATE_MINIMIZED);
             this.mCurrentBrowser = newBrowser;
             this.mCurrentTab = this.tabContainer.selectedItem;
             this.showTab(this.mCurrentTab);
 
-            var backForwardContainer = document.getElementById("urlbar-container");
-            if (backForwardContainer) {
-              backForwardContainer.setAttribute("switchingtabs", "true");
+            var forwardButtonContainer = document.getElementById("urlbar-wrapper");
+            if (forwardButtonContainer) {
+              forwardButtonContainer.setAttribute("switchingtabs", "true");
               window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() {
                 window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr);
-                backForwardContainer.removeAttribute("switchingtabs");
+                forwardButtonContainer.removeAttribute("switchingtabs");
               });
             }
 
             this._appendStatusPanel();
 
             if (updateBlockedPopups)
               this.mCurrentBrowser.updateBlockedPopups(false);
 
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -85,67 +85,68 @@ let gTests = [
        "Search engine logo's alt text is a nonempty string");
 
     isnot(altText, "undefined",
           "Search engine logo's alt text shouldn't be the string 'undefined'");
   }
 },
 
 // Disabled on Linux for intermittent issues with FHR, see Bug 945667.
-// Disabled always due to bug 992485
 {
   desc: "Check that performing a search fires a search event and records to " +
         "Firefox Health Report.",
   setup: function () { },
   run: function () {
-    // Skip this test always for now since it loads google.com and that causes bug 992485
-    return;
-
     // Skip this test on Linux.
     if (navigator.platform.indexOf("Linux") == 0) { return; }
 
     try {
       let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
       cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
     } catch (ex) {
       // Health Report disabled, or no SearchesProvider.
       return Promise.resolve();
     }
 
     let numSearchesBefore = 0;
-    let deferred = Promise.defer();
+    let searchEventDeferred = Promise.defer();
     let doc = gBrowser.contentDocument;
     let engineName = doc.documentElement.getAttribute("searchEngineName");
 
     doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) {
       let data = JSON.parse(e.detail);
       is(data.engineName, engineName, "Detail is search engine name");
 
       // We use executeSoon() to ensure that this code runs after the
       // count has been updated in browser.js, since it uses the same
       // event.
       executeSoon(function () {
         getNumberOfSearches(engineName).then(num => {
           is(num, numSearchesBefore + 1, "One more search recorded.");
-          deferred.resolve();
+          searchEventDeferred.resolve();
         });
       });
     }, true, true);
 
     // Get the current number of recorded searches.
+    let searchStr = "a search";
     getNumberOfSearches(engineName).then(num => {
       numSearchesBefore = num;
 
       info("Perform a search.");
-      doc.getElementById("searchText").value = "a search";
+      doc.getElementById("searchText").value = searchStr;
       doc.getElementById("searchSubmit").click();
-      gBrowser.stop();
     });
 
-    return deferred.promise;
+    let expectedURL = Services.search.currentEngine.
+                      getSubmission(searchStr, null, "homepage").
+                      uri.spec;
+    let loadPromise = waitForDocLoadAndStopIt(expectedURL);
+
+    return Promise.all([searchEventDeferred.promise, loadPromise]);
   }
 },
 
 {
   desc: "Check snippets map is cleared if cached version is old",
   setup: function (aSnippetsMap)
   {
     aSnippetsMap.set("snippets", "test");
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -287,16 +287,48 @@ function promiseHistoryClearedState(aURI
          "history visit " + aURI.spec + " should " + niceStr + " exist");
       callbackDone();
     });
   });
 
   return deferred.promise;
 }
 
+/**
+ * Waits for the next top-level document load in the current browser.  The URI
+ * of the document is compared against aExpectedURL.  The load is then stopped
+ * before it actually starts.
+ *
+ * @param aExpectedURL
+ *        The URL of the document that is expected to load.
+ * @return promise
+ */
+function waitForDocLoadAndStopIt(aExpectedURL) {
+  let deferred = Promise.defer();
+  let progressListener = {
+    onStateChange: function (webProgress, req, flags, status) {
+      info("waitForDocLoadAndStopIt: onStateChange: " + req.name);
+      let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+                     Ci.nsIWebProgressListener.STATE_START;
+      if ((flags & docStart) && webProgress.isTopLevel) {
+        info("waitForDocLoadAndStopIt: Document start: " +
+             req.QueryInterface(Ci.nsIChannel).URI.spec);
+        is(req.originalURI.spec, aExpectedURL,
+           "waitForDocLoadAndStopIt: The expected URL was loaded");
+        req.cancel(Components.results.NS_ERROR_FAILURE);
+        gBrowser.removeProgressListener(progressListener);
+        deferred.resolve();
+      }
+    },
+  };
+  gBrowser.addProgressListener(progressListener);
+  info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
+  return deferred.promise;
+}
+
 let FullZoomHelper = {
 
   selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
     if (!tab)
       throw new Error("tab must be given.");
     if (gBrowser.selectedTab == tab)
       return Promise.resolve();
     gBrowser.selectedTab = tab;
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -99,16 +99,20 @@
       <richlistbox id="downloadsListBox"
                    class="plain"
                    flex="1"
                    context="downloadsContextMenu"
                    onmouseover="DownloadsView.onDownloadMouseOver(event);"
                    onmouseout="DownloadsView.onDownloadMouseOut(event);"
                    oncontextmenu="DownloadsView.onDownloadContextMenu(event);"
                    ondragstart="DownloadsView.onDownloadDragStart(event);"/>
+      <description id="emptyDownloads"
+                   mousethrough="always">
+         &downloadsPanelEmpty.label;
+      </description>
 
       <vbox id="downloadsFooter">
         <hbox id="downloadsSummary"
               align="center"
               orient="horizontal"
               onkeydown="DownloadsSummary.onKeyDown(event);"
               onclick="DownloadsSummary.onClick(event);">
           <image class="downloadTypeIcon" />
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -79,13 +79,18 @@
 <!ENTITY clearDownloadsButton.tooltip     "Clears completed, canceled and failed downloads">
 
 <!-- LOCALIZATION NOTE (downloadsListEmpty.label):
      This string is shown when there are no items in the Downloads view, when it
      is displayed inside a browser tab.
      -->
 <!ENTITY downloadsListEmpty.label         "There are no downloads.">
 
+<!-- LOCALIZATION NOTE (downloadsPanelEmpty.label):
+     This string is shown when there are no items in the Downloads Panel.
+     -->
+<!ENTITY downloadsPanelEmpty.label        "No downloads for this session.">
+
 <!-- LOCALIZATION NOTE (downloadsListNoMatch.label):
      This string is shown when some search terms are specified, but there are no
      results in the Downloads view.
      -->
 <!ENTITY downloadsListNoMatch.label       "Could not find any matching downloads.">
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -10,17 +10,17 @@
 @namespace html url("http://www.w3.org/1999/xhtml");
 @namespace svg url("http://www.w3.org/2000/svg");
 
 %include ../shared/browser.inc
 %include linuxShared.inc
 %filter substitution
 
 %define forwardTransitionLength 150ms
-%define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-container
+%define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-wrapper
 %define conditionalForwardWithUrlbarWidth 30
 
 #menubar-items {
   -moz-box-orient: vertical; /* for flex hack */
 }
 
 #main-menubar {
   -moz-box-flex: 1; /* make menu items expand to fill toolbar height */
@@ -730,45 +730,49 @@ toolbarbutton[sdk-button="true"][cui-are
 }
 
 #main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
   box-shadow: 0 0 0 1px hsla(210,54%,20%,.55),
               0 1px 0 hsla(210,54%,20%,.65) !important;
   transition: none;
 }
 
-#back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
-#forward-button:-moz-locale-dir(rtl) {
+#back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon {
   transform: scaleX(-1);
 }
 
-@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] {
-  opacity: 0;
-}
-
-@conditionalForwardWithUrlbar@:not([switchingtabs]) > #forward-button {
-  transition: opacity @forwardTransitionLength@ ease-out;
-}
-
-@conditionalForwardWithUrlbar@ > #forward-button[occluded-by-urlbar] {
-  visibility: hidden;
-}
-
 #forward-button {
   padding: 0;
 }
 
 #forward-button > .toolbarbutton-icon {
   background-clip: padding-box;
-  clip-path: url("chrome://browser/content/browser.xul#keyhole-forward-clip-path");
-  margin-left: -6px;
+  padding-left: 9px;
+  padding-right: 3px;
+  border: 1px solid #9a9a9a;
   border-left-style: none;
   border-radius: 0;
-  padding: 2px 3px 2px 9px;
-  border: 1px solid #9a9a9a;
+}
+
+@conditionalForwardWithUrlbar@:not([switchingtabs]) > #forward-button {
+  transition: margin-left @forwardTransitionLength@ ease-out;
+}
+
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] {
+  margin-left: -@conditionalForwardWithUrlbarWidth@px;
+}
+
+@conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] {
+  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
+  transition-delay: 100s;
+}
+
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] {
+  /* when not hovered anymore, trigger a new transition to hide the forward button immediately */
+  margin-left: -@conditionalForwardWithUrlbarWidth@.01px;
 }
 
 /* tabview menu item */
 
 #menu_tabview {
   list-style-image: url(chrome://browser/skin/tabview/tabview.png);
   -moz-image-region: rect(0, 80px, 16px, 64px);
 }
@@ -898,67 +902,44 @@ toolbarbutton[sdk-button="true"][cui-are
 .urlbar-history-dropmarker {
   -moz-appearance: toolbarbutton-dropdown;
 }
 
 #urlbar-container {
   -moz-box-align: center;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper {
-  padding-left: @conditionalForwardWithUrlbarWidth@px;
-  -moz-margin-start: -@conditionalForwardWithUrlbarWidth@px;
-  position: relative;
-  pointer-events: none;
+@conditionalForwardWithUrlbar@ > #urlbar {
+  -moz-border-start: none;
+  margin-left: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar {
-  -moz-border-start: none;
-  margin-left: 0;
-  pointer-events: all;
-}
-
-@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar-wrapper > #urlbar {
-  transition: margin-left @forwardTransitionLength@ ease-out;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(ltr) {
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(ltr) {
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(rtl) {
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper {
+@conditionalForwardWithUrlbar@ {
   clip-path: url("chrome://browser/content/browser.xul#urlbar-back-button-clip-path");
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar {
-  margin-left: -@conditionalForwardWithUrlbarWidth@px;
+  -moz-margin-start: -5px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled]:hover:not([switchingtabs]) > #urlbar-wrapper > #urlbar {
-  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
-  transition-delay: 100s;
+@conditionalForwardWithUrlbar@:-moz-locale-dir(rtl),
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
+  /* let urlbar-back-button-clip-path clip the urlbar's right side for RTL */
+  transform: scaleX(-1);
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar,
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar {
-  /* when switching tabs, or when not hovered anymore, trigger a new transition
-   * to hide the forward button immediately */
-  margin-left: -@conditionalForwardWithUrlbarWidth@.01px;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper:-moz-locale-dir(rtl),
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(rtl) {
-  /* let windows-urlbar-back-button-mask clip the urlbar's right side for RTL */
-  transform: scaleX(-1);
+@conditionalForwardWithUrlbar@:-moz-locale-dir(rtl) {
+  -moz-box-direction: reverse;
 }
 
 #urlbar-icons {
   -moz-box-align: center;
 }
 
 .urlbar-icon {
   cursor: pointer;
@@ -1010,43 +991,43 @@ toolbarbutton[sdk-button="true"][cui-are
   border-bottom-right-radius: 1.5px;
 }
 
 #notification-popup-box:not([hidden]) + #identity-box {
   -moz-padding-start: 10px;
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar > #identity-box {
+@conditionalForwardWithUrlbar@ > #urlbar > #identity-box {
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
-  padding-left: 5px;
-  transition: padding-left;
+@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar > #identity-box {
+  transition: padding-left, padding-right;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
-  padding-right: 5px;
-  transition: padding-right;
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
+  padding-left: 5px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled]:hover:not([switchingtabs]) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box {
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
+  padding-right: 5px;
+}
+
+@conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box {
   /* forward button hiding is delayed when hovered */
   transition-delay: 100s;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr),
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
   /* when not hovered anymore, trigger a new non-delayed transition to react to the forward button hiding */
   padding-left: 5.01px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl),
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
   /* when not hovered anymore, trigger a new non-delayed transition to react to the forward button hiding */
   padding-right: 5.01px;
 }
 
 #urlbar[pageproxystate="valid"] > #identity-box.chromeUI,
 #urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity {
   -moz-margin-end: 4px;
 }
@@ -1066,24 +1047,20 @@ toolbarbutton[sdk-button="true"][cui-are
 }
 
 %include ../shared/identity-block.inc.css
 
 #page-proxy-favicon {
   margin-top: 1px;
   margin-bottom: 1px;
   -moz-margin-start: 3px;
-  -moz-margin-end: 2px;
+  -moz-margin-end: 1px;
   -moz-image-region: rect(0, 16px, 16px, 0);
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar > #identity-box > #page-proxy-favicon {
-  -moz-margin-end: 1px;
-}
-
 #identity-box:hover > #page-proxy-favicon {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
 #identity-box:hover:active > #page-proxy-favicon,
 #identity-box[open=true] > #page-proxy-favicon {
   -moz-image-region: rect(0, 48px, 16px, 32px);
 }
--- a/browser/themes/linux/downloads/downloads.css
+++ b/browser/themes/linux/downloads/downloads.css
@@ -13,23 +13,32 @@
   padding: 4px;
   color: inherit;
 }
 
 #downloadsPanel:not([hasdownloads]) > #downloadsListBox {
   display: none;
 }
 
+#downloadsPanel[hasdownloads] > #emptyDownloads {
+  display: none;
+}
+
+#emptyDownloads {
+  padding: 10px 20px;
+  max-width: 40ch;
+}
+
 #downloadsHistory {
   background: transparent;
   color: -moz-nativehyperlinktext;
   cursor: pointer;
 }
 
-#downloadsPanel[hasdownloads] > #downloadsFooter {
+#downloadsFooter {
   border-top: 1px solid ThreeDShadow;
   background-image: linear-gradient(hsla(0,0%,0%,.15), hsla(0,0%,0%,.08) 6px);
 }
 
 #downloadsHistory > .button-box {
   margin: 1em;
 }
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -2,18 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @import url("chrome://global/skin/");
 
 %include shared.inc
 %filter substitution
 %define forwardTransitionLength 150ms
-%define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-container
-%define conditionalForwardWithUrlbarWidth 30
+%define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-wrapper
+%define conditionalForwardWithUrlbarWidth 32
 %define spaceAboveTabbar 9px
 %define toolbarButtonPressed :hover:active:not([disabled="true"]):not([cui-areatype="menu-panel"])
 %define windowButtonMarginTop 11px
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 @namespace svg url("http://www.w3.org/2000/svg");
 
@@ -1415,30 +1415,27 @@ toolbarbutton[sdk-button="true"][cui-are
 }
 
 #back-button:-moz-window-inactive,
 #forward-button:-moz-window-inactive {
   background-color: rgba(0,0,0,0.04);
   border-color: rgba(0,0,0,0.2);
 }
 
-#back-button:-moz-locale-dir(rtl),
-#forward-button:-moz-locale-dir(rtl) {
+#back-button:-moz-locale-dir(rtl) {
   transform: scaleX(-1);
 }
 
 /* Back button styles */
 
 #back-button {
-  -moz-margin-end: -7px;
-  position: relative;
-  z-index: 1;
   width: 32px;
   height: 32px;
   padding: 4px 5px 4px 3px;
+  -moz-margin-end: 0;
   border-radius: 10000px;
 }
 
 #back-button:not(:-moz-lwtheme) {
   height: 33px;
   padding: 4px 5px 5px 3px;
   margin-bottom: -1px;
   background: url(chrome://browser/skin/keyhole-circle.png) 0 0 no-repeat;
@@ -1458,68 +1455,50 @@ toolbarbutton[sdk-button="true"][cui-are
 #back-button:not([disabled="true"]):active:hover:not(:-moz-lwtheme),
 #back-button[open="true"]:not(:-moz-lwtheme) {
   background-position: -32px 0;
 }
 
 /* Forward button styles */
 
 #forward-button {
-  -moz-margin-start: 0;
-  -moz-margin-end: 0;
+  margin-left: -2px;
+  margin-right: 0;
+  padding-left: 2px;
   width: 32px;
-  clip-path: url(chrome://browser/content/browser.xul#osx-keyhole-forward-clip-path);
 }
 
 #forward-button > .toolbarbutton-icon {
   /* shift the icon away from the back button */
   margin-left: 3px;
   margin-right: -1px;
 }
 
-#forward-button:-moz-lwtheme {
-  -moz-padding-start: 2px;
-  -moz-padding-end: 0;
-}
-
 #forward-button:not(:-moz-lwtheme) {
-  -moz-padding-start: 2px;
   background: linear-gradient(hsl(0,0%,99%), hsl(0,0%,67%)) padding-box;
   border: 1px solid;
   border-color: hsl(0,0%,31%) hsla(0,0%,29%,.6) hsl(0,0%,27%);
   box-shadow: inset 0 1px 0 hsla(0,0%,100%,.35),
               0 1px 0 hsla(0,0%,100%,.2);
 }
 
-#urlbar-container:not([switchingtabs]) > #forward-button {
-  transition: opacity @forwardTransitionLength@ ease-out;
-}
-
 #forward-button:hover:active:not(:-moz-lwtheme) {
   background-image: linear-gradient(hsl(0,0%,74%), hsl(0,0%,61%));
   box-shadow: inset rgba(0,0,0,.3) 0 -6px 10px,
               inset #000 0 1px 3px,
               inset rgba(0,0,0,.2) 0 1px 3px,
               0 1px 0 hsla(0,0%,100%,.2);
 }
 
 #forward-button:-moz-window-inactive:not(:-moz-lwtheme) {
   border-color: hsl(0,0%,64%) hsl(0,0%,65%) hsl(0,0%,66%);
   background-image: linear-gradient(hsl(0,0%,99%), hsl(0,0%,82%));
   box-shadow: inset 0 1px 0 hsla(0,0%,100%,.35);
 }
 
-#urlbar-container:not(:hover) > #forward-button[disabled] {
-  opacity: 0;
-}
-
-@conditionalForwardWithUrlbar@ > #forward-button[occluded-by-urlbar] {
-  visibility: hidden;
-}
-
 @media (-moz-mac-lion-theme) {
   #forward-button:not(:-moz-lwtheme) {
     background-image: linear-gradient(hsla(0,0%,100%,.73), hsla(0,0%,100%,.05) 85%);
     border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.25) hsla(0,0%,0%,.2);
     box-shadow: inset 0 1px 0 hsla(0,0%,100%,.2),
                 inset 0 0 1px hsla(0,0%,100%,.1),
                 0 1px 0 hsla(0,0%,100%,.2);
   }
@@ -1533,16 +1512,34 @@ toolbarbutton[sdk-button="true"][cui-are
   }
 
   #forward-button:-moz-window-inactive:not(:-moz-lwtheme) {
     background-image: none;
     border-color: hsla(0,0%,0%,.2);
   }
 }
 
+@conditionalForwardWithUrlbar@:not([switchingtabs]) > #forward-button {
+  transition: margin-left @forwardTransitionLength@ ease-out;
+}
+
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] {
+  margin-left: -@conditionalForwardWithUrlbarWidth@px;
+}
+
+@conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] {
+  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
+  transition-delay: 100s;
+}
+
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] {
+  /* when not hovered anymore, trigger a new transition to hide the forward button immediately */
+  margin-left: -@conditionalForwardWithUrlbarWidth@.01px;
+}
+
 .unified-nav-back[_moz-menuactive]:-moz-locale-dir(ltr),
 .unified-nav-forward[_moz-menuactive]:-moz-locale-dir(rtl) {
   list-style-image: url("chrome://browser/skin/menu-back.png") !important;
 }
 
 .unified-nav-forward[_moz-menuactive]:-moz-locale-dir(ltr),
 .unified-nav-back[_moz-menuactive]:-moz-locale-dir(rtl) {
   list-style-image: url("chrome://browser/skin/menu-forward.png") !important;
@@ -1716,69 +1713,46 @@ toolbarbutton[sdk-button="true"][cui-are
   -moz-box-align: center;
 }
 
 #urlbar {
   -moz-padding-end: 4px;
   border-radius: @toolbarbuttonCornerRadius@;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper {
-  padding-left: @conditionalForwardWithUrlbarWidth@px;
-  -moz-margin-start: -@conditionalForwardWithUrlbarWidth@px;
-  position: relative;
-  pointer-events: none;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar {
+@conditionalForwardWithUrlbar@ > #urlbar {
   -moz-border-start: none;
   margin-left: 0;
-  pointer-events: all;
-}
-
-@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar-wrapper > #urlbar {
-  transition: margin-left @forwardTransitionLength@ ease-out;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(ltr) {
+}
+
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(ltr) {
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(rtl) {
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper {
-  clip-path: url("chrome://browser/content/browser.xul#osx-urlbar-back-button-clip-path");
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar {
-  margin-left: -@conditionalForwardWithUrlbarWidth@px;
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled]:hover:not([switchingtabs]) > #urlbar-wrapper > #urlbar {
-  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
-  transition-delay: 100s;
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar,
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar {
-  /* when switching tabs, or when not hovered anymore, trigger a new transition
-   * to hide the forward button immediately */
-  margin-left: -@conditionalForwardWithUrlbarWidth@.01px;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper:-moz-locale-dir(rtl),
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(rtl) {
-  /* let osx-urlbar-back-button-clip-path clip the urlbar's right side for RTL */
+@conditionalForwardWithUrlbar@ {
+  clip-path: url("chrome://browser/content/browser.xul#urlbar-back-button-clip-path");
+  -moz-margin-start: -6px;
+}
+
+@conditionalForwardWithUrlbar@:-moz-locale-dir(rtl),
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
+  /* let urlbar-back-button-clip-path clip the urlbar's right side for RTL */
   transform: scaleX(-1);
 }
 
+@conditionalForwardWithUrlbar@:-moz-locale-dir(rtl) {
+  -moz-box-direction: reverse;
+}
+
 #identity-box {
   -moz-margin-end: 3px;
   padding-top: 1px;
   padding-bottom: 1px;
   -moz-padding-start: 4px;
   -moz-padding-end: 0;
   font-size: .9em;
 }
@@ -1793,42 +1767,42 @@ toolbarbutton[sdk-button="true"][cui-are
   border-bottom-right-radius: 2px;
 }
 
 #notification-popup-box:not([hidden]) + #identity-box {
   -moz-padding-start: 10px;
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar > #identity-box {
+@conditionalForwardWithUrlbar@ > #urlbar > #identity-box {
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
-  transition: 0s padding-left;
+@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar > #identity-box {
+  transition: padding-left, padding-right;
+}
+
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
   padding-left: 10px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
-  transition: 0s padding-right;
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
   padding-right: 10px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled]:hover:not([switchingtabs]) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box {
-  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
+@conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box {
+  /* forward button hiding is delayed when hovered */
   transition-delay: 100s;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr),
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
   padding-left: 10.01px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl),
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
   padding-right: 10.01px;
 }
 
 #urlbar[pageproxystate="valid"] > #identity-box.chromeUI,
 #urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity {
   -moz-padding-end: 4px;
 }
 
@@ -3395,17 +3369,17 @@ toolbarbutton.chevron > .toolbarbutton-m
 }
 
 @media (min-resolution: 2dppx) {
   #notification-popup-box {
     border-image: url("chrome://browser/skin/urlbar-arrow@2x.png") 0 16 0 0 fill;
   }
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box {
+@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar > #notification-popup-box {
   padding-left: 7px;
 }
 
 #notification-popup-box:-moz-locale-dir(rtl),
 .notification-anchor-icon:-moz-locale-dir(rtl) {
   transform: scaleX(-1);
 }
 
--- a/browser/themes/osx/downloads/downloads.css
+++ b/browser/themes/osx/downloads/downloads.css
@@ -17,28 +17,37 @@
   padding: 4px;
   color: inherit;
 }
 
 #downloadsPanel:not([hasdownloads]) > #downloadsListBox {
   display: none;
 }
 
+#downloadsPanel[hasdownloads] > #emptyDownloads {
+  display: none;
+}
+
+#emptyDownloads {
+  padding: 10px 20px;
+  max-width: 40ch;
+}
+
 #downloadsFooter {
   border-bottom-left-radius: 4px;
   border-bottom-right-radius: 4px;
 }
 
 #downloadsHistory {
   background: transparent;
   color: hsl(210,100%,75%);
   cursor: pointer;
 }
 
-#downloadsPanel[hasdownloads] > #downloadsFooter {
+#downloadsFooter {
   background: #e5e5e5;
   border-top: 1px solid hsla(0,0%,0%,.1);
   box-shadow: 0 -1px hsla(0,0%,100%,.5) inset, 0 1px 1px hsla(0,0%,0%,.03) inset;
 }
 
 #downloadsHistory > .button-box {
   color: #808080;
   margin: 1em;
@@ -47,21 +56,16 @@
 #downloadsPanel[keyfocus] > #downloadsFooter > #downloadsSummary:focus,
 #downloadsPanel[keyfocus] > #downloadsFooter > #downloadsHistory:focus {
   outline: 2px -moz-mac-focusring solid;
   outline-offset: -2px;
   -moz-outline-radius-bottomleft: 4px;
   -moz-outline-radius-bottomright: 4px;
 }
 
-#downloadsPanel:not([hasdownloads]) > #downloadsFooter > #downloadsHistory:focus {
-  -moz-outline-radius-topleft: 4px;
-  -moz-outline-radius-topright: 4px;
-}
-
 /*** Downloads Summary and List items ***/
 
 #downloadsSummary,
 richlistitem[type="download"] {
   height: 7em;
   -moz-padding-end: 0;
   color: inherit;
 }
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -9,17 +9,17 @@
 @namespace svg url("http://www.w3.org/2000/svg");
 
 %include ../shared/browser.inc
 %include windowsShared.inc
 %filter substitution
 %define toolbarShadowColor hsla(209,67%,12%,0.35)
 %define navbarTextboxCustomBorder border-color: rgba(0,0,0,.32);
 %define forwardTransitionLength 150ms
-%define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-container
+%define conditionalForwardWithUrlbar window:not([chromehidden~="toolbar"]) #urlbar-wrapper
 %define conditionalForwardWithUrlbarWidth 30
 
 #menubar-items {
   -moz-box-orient: vertical; /* for flex hack */
 }
 
 #main-menubar {
   -moz-box-flex: 1; /* make menu items expand to fill toolbar height */
@@ -881,46 +881,38 @@ toolbarbutton[sdk-button="true"][cui-are
 }
 
 #forward-button > menupopup {
   margin-top: 1px !important;
 }
 
 #forward-button > .toolbarbutton-icon {
   background-clip: padding-box !important;
-  /*mask: url(keyhole-forward-mask.svg#mask); XXX: this regresses twinopen */
-  clip-path: url(chrome://browser/content/browser.xul#keyhole-forward-clip-path) !important;
-  margin-left: -6px !important;
   border-left-style: none !important;
   border-radius: 0 !important;
   padding-left: 9px !important;
   padding-right: 3px !important;
 }
 
-%ifdef WINDOWS_AERO
-@media (-moz-os-version: windows-vista),
-       (-moz-os-version: windows-win7) {
-%endif
-  #forward-button > .toolbarbutton-icon {
-    margin-left: -6px !important;
-  }
-%ifdef WINDOWS_AERO
-}
-%endif
-
 @conditionalForwardWithUrlbar@:not([switchingtabs]) > #forward-button {
-  transition: opacity @forwardTransitionLength@ ease-out;
+  transition: margin-left @forwardTransitionLength@ ease-out;
+}
+
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] {
+  margin-left: -@conditionalForwardWithUrlbarWidth@px;
+}
+
+@conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] {
+  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
+  transition-delay: 100s;
 }
 
 @conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] {
-  opacity: 0;
-}
-
-@conditionalForwardWithUrlbar@ > #forward-button[occluded-by-urlbar] {
-  visibility: hidden;
+  /* when not hovered anymore, trigger a new transition to hide the forward button immediately */
+  margin-left: -@conditionalForwardWithUrlbarWidth@.01px;
 }
 
 #back-button {
   padding-top: 3px !important;
   padding-bottom: 3px !important;
   -moz-padding-start: 5px !important;
   -moz-padding-end: 0 !important;
   position: relative !important;
@@ -994,18 +986,17 @@ toolbarbutton[sdk-button="true"][cui-are
     box-shadow: 0 0 0 1px hsla(210,54%,20%,.55),
                 0 1px 0 hsla(210,54%,20%,.65) !important;
     transition: none;
   }
 %ifdef WINDOWS_AERO
 }
 %endif
 
-#back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
-#forward-button:-moz-locale-dir(rtl) {
+#back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon {
   transform: scaleX(-1);
 }
 
 .unified-nav-back[_moz-menuactive]:-moz-locale-dir(ltr),
 .unified-nav-forward[_moz-menuactive]:-moz-locale-dir(rtl) {
   list-style-image: url("chrome://browser/skin/menu-back.png") !important;
 }
 
@@ -1167,69 +1158,46 @@ toolbarbutton[sdk-button="true"][cui-are
   background-color: rgba(255,255,255,.9);
 }
 
 #urlbar:-moz-lwtheme[focused]:not([readonly]),
 .searchbar-textbox:-moz-lwtheme[focused] {
   background-color: white;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper {
-  padding-left: @conditionalForwardWithUrlbarWidth@px;
-  -moz-margin-start: -@conditionalForwardWithUrlbarWidth@px;
-  position: relative;
-  pointer-events: none;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar {
+@conditionalForwardWithUrlbar@ > #urlbar {
   -moz-border-start: none;
   margin-left: 0;
-  pointer-events: all;
-}
-
-@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar-wrapper > #urlbar {
-  transition: margin-left @forwardTransitionLength@ ease-out;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(ltr) {
+}
+
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(ltr) {
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(rtl) {
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper {
+@conditionalForwardWithUrlbar@ {
   clip-path: url("chrome://browser/content/browser.xul#urlbar-back-button-clip-path");
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar {
-  margin-left: -@conditionalForwardWithUrlbarWidth@px;
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled]:hover:not([switchingtabs]) > #urlbar-wrapper > #urlbar {
-  /* delay the hiding of the forward button when hovered to avoid accidental clicks on the url bar */
-  transition-delay: 100s;
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar,
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar {
-  /* when switching tabs, or when not hovered anymore, trigger a new transition
-   * to hide the forward button immediately */
-  margin-left: -@conditionalForwardWithUrlbarWidth@.01px;
-}
-
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper:-moz-locale-dir(rtl),
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar:-moz-locale-dir(rtl) {
-  /* let windows-urlbar-back-button-mask clip the urlbar's right side for RTL */
+  -moz-margin-start: -5px;
+}
+
+@conditionalForwardWithUrlbar@:-moz-locale-dir(rtl),
+@conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
+  /* let urlbar-back-button-clip-path clip the urlbar's right side for RTL */
   transform: scaleX(-1);
 }
 
+@conditionalForwardWithUrlbar@:-moz-locale-dir(rtl) {
+  -moz-box-direction: reverse;
+}
+
 html|*.urlbar-input:-moz-lwtheme::-moz-placeholder,
 .searchbar-textbox:-moz-lwtheme > .autocomplete-textbox-container > .textbox-input-box > html|*.textbox-input::-moz-placeholder {
   opacity: 1.0;
   color: #777;
 }
 
 #urlbar-container {
   -moz-box-align: center;
@@ -1309,43 +1277,43 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
   border-bottom-right-radius: 1.5px;
 }
 
 #notification-popup-box:not([hidden]) + #identity-box {
   -moz-padding-start: 10px;
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar > #identity-box {
+@conditionalForwardWithUrlbar@ > #urlbar > #identity-box {
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
+@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar > #identity-box {
+  transition: padding-left, padding-right;
+}
+
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
   padding-left: 5px;
-  transition: padding-left;
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
+}
+
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
   padding-right: 5px;
-  transition: padding-right;
-}
-
-@conditionalForwardWithUrlbar@[forwarddisabled]:hover:not([switchingtabs]) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box {
+}
+
+@conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box {
   /* forward button hiding is delayed when hovered */
   transition-delay: 100s;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr),
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(ltr) {
   /* when not hovered anymore, trigger a new non-delayed transition to react to the forward button hiding */
   padding-left: 5.01px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled][switchingtabs] + #urlbar-container > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl),
-@conditionalForwardWithUrlbar@[forwarddisabled]:not(:hover) > #urlbar-wrapper > #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
+@conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box:-moz-locale-dir(rtl) {
   /* when not hovered anymore, trigger a new non-delayed transition to react to the forward button hiding */
   padding-right: 5.01px;
 }
 
 #urlbar[pageproxystate="valid"] > #identity-box.chromeUI,
 #urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity {
   -moz-margin-end: 4px;
 }
@@ -1390,24 +1358,20 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 /* page proxy icon */
 
 %include ../shared/identity-block.inc.css
 
 #page-proxy-favicon {
   margin-top: 1px;
   margin-bottom: 1px;
   -moz-margin-start: 3px;
-  -moz-margin-end: 2px;
+  -moz-margin-end: 1px;
   -moz-image-region: rect(0, 16px, 16px, 0);
 }
 
-@conditionalForwardWithUrlbar@ > #urlbar-wrapper > #urlbar > #identity-box > #page-proxy-favicon {
-  -moz-margin-end: 1px;
-}
-
 #identity-box:hover > #page-proxy-favicon {
   -moz-image-region: rect(0, 32px, 16px, 16px);
 }
 
 #identity-box:hover:active > #page-proxy-favicon,
 #identity-box[open=true] > #page-proxy-favicon {
   -moz-image-region: rect(0, 48px, 16px, 32px);
 }
--- a/browser/themes/windows/downloads/downloads.css
+++ b/browser/themes/windows/downloads/downloads.css
@@ -13,16 +13,25 @@
   padding: 4px;
   color: inherit;
 }
 
 #downloadsPanel:not([hasdownloads]) > #downloadsListBox {
   display: none;
 }
 
+#downloadsPanel[hasdownloads] > #emptyDownloads {
+  display: none;
+}
+
+#emptyDownloads {
+  padding: 10px 20px;
+  max-width: 40ch;
+}
+
 #downloadsHistory {
   background: transparent;
   cursor: pointer;
 }
 
 %ifdef WINDOWS_AERO
 @media (-moz-os-version: windows-vista),
        (-moz-os-version: windows-win7) {
@@ -39,46 +48,46 @@
   outline-offset: -1px;
 }
 
 #downloadsHistory > .button-box {
   border: none;
   margin: 1em;
 }
 
-#downloadsPanel[hasdownloads] > #downloadsFooter {
+#downloadsFooter {
   background-color: hsla(210,4%,10%,.04);
   box-shadow: 0 1px 0 hsla(210,4%,10%,.08) inset;
   transition-duration: 150ms;
   transition-property: background-color;
 }
 
-#downloadsPanel[hasdownloads] > #downloadsFooter:hover {
+#downloadsFooter:hover {
   background-color: hsla(210,4%,10%,.05);
 }
 
-#downloadsPanel[hasdownloads] > #downloadsFooter:hover:active {
+#downloadsFooter:hover:active {
   background-color: hsla(210,4%,10%,.1);
   box-shadow: 0 2px 0 0 hsla(210,4%,10%,.1) inset;
 }
 
 %ifdef WINDOWS_AERO
 @media (-moz-os-version: windows-vista),
        (-moz-os-version: windows-win7) {
 %endif
 @media (-moz-windows-default-theme) {
-  #downloadsPanel[hasdownloads] > #downloadsFooter {
+  #downloadsFooter {
     border-bottom-left-radius: 3px;
     border-bottom-right-radius: 3px;
     transition-duration: 0s;
   }
 
-  #downloadsPanel[hasdownloads] > #downloadsFooter,
-  #downloadsPanel[hasdownloads] > #downloadsFooter:hover,
-  #downloadsPanel[hasdownloads] > #downloadsFooter:hover:active {
+  #downloadsFooter,
+  #downloadsFooter:hover,
+  #downloadsFooter:hover:active {
 %ifdef WINDOWS_AERO
     background-color: #f1f5fb;
 %else
     background-color: hsla(216,45%,88%,.98);
 %endif
     box-shadow: 0px 1px 2px rgb(204,214,234) inset;
   }
 }
--- a/dom/apps/src/InterAppCommService.js
+++ b/dom/apps/src/InterAppCommService.js
@@ -1,874 +1,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/InterAppCommService.jsm");
 
-const DEBUG = false;
-function debug(aMsg) {
-  dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n");
+function InterAppCommServiceProxy() {
 }
 
-XPCOMUtils.defineLazyServiceGetter(this, "appsService",
-                                   "@mozilla.org/AppsService;1",
-                                   "nsIAppsService");
-
-XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
-                                   "@mozilla.org/parentprocessmessagemanager;1",
-                                   "nsIMessageBroadcaster");
-
-XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
-
-XPCOMUtils.defineLazyServiceGetter(this, "messenger",
-                                   "@mozilla.org/system-message-internal;1",
-                                   "nsISystemMessagesInternal");
-
-const kMessages =["Webapps:Connect",
-                  "Webapps:GetConnections",
-                  "InterAppConnection:Cancel",
-                  "InterAppMessagePort:PostMessage",
-                  "InterAppMessagePort:Register",
-                  "InterAppMessagePort:Unregister",
-                  "child-process-shutdown"];
-
-function InterAppCommService() {
-  Services.obs.addObserver(this, "xpcom-shutdown", false);
-  Services.obs.addObserver(this, "inter-app-comm-select-app-result", false);
-
-  kMessages.forEach(function(aMsg) {
-    ppmm.addMessageListener(aMsg, this);
-  }, this);
-
-  // This matrix is used for saving the inter-app connection info registered in
-  // the app manifest. The object literal is defined as below:
-  //
-  // {
-  //   "keyword1": {
-  //     "subAppManifestURL1": {
-  //       /* subscribed info */
-  //     },
-  //     "subAppManifestURL2": {
-  //       /* subscribed info */
-  //     },
-  //     ...
-  //   },
-  //   "keyword2": {
-  //     "subAppManifestURL3": {
-  //       /* subscribed info */
-  //     },
-  //     ...
-  //   },
-  //   ...
-  // }
-  //
-  // For example:
-  //
-  // {
-  //   "foo": {
-  //     "app://subApp1.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp1.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     },
-  //     "app://subApp2.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp2.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     }
-  //   },
-  //   "bar": {
-  //     "app://subApp3.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp3.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     }
-  //   }
-  // }
-  //
-  // TODO Bug 908999 - Update registered connections when app gets uninstalled.
-  this._registeredConnections = {};
-
-  // This matrix is used for saving the permitted connections, which allows
-  // the messaging between publishers and subscribers. The object literal is
-  // defined as below:
-  //
-  // {
-  //   "keyword1": {
-  //     "pubAppManifestURL1": [
-  //       "subAppManifestURL1",
-  //       "subAppManifestURL2",
-  //       ...
-  //     ],
-  //     "pubAppManifestURL2": [
-  //       "subAppManifestURL3",
-  //       "subAppManifestURL4",
-  //       ...
-  //     ],
-  //     ...
-  //   },
-  //   "keyword2": {
-  //     "pubAppManifestURL3": [
-  //       "subAppManifestURL5",
-  //       ...
-  //     ],
-  //     ...
-  //   },
-  //   ...
-  // }
-  //
-  // For example:
-  //
-  // {
-  //   "foo": {
-  //     "app://pubApp1.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp1.gaiamobile.org/manifest.webapp",
-  //       "app://subApp2.gaiamobile.org/manifest.webapp"
-  //     ],
-  //     "app://pubApp2.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp3.gaiamobile.org/manifest.webapp",
-  //       "app://subApp4.gaiamobile.org/manifest.webapp"
-  //     ]
-  //   },
-  //   "bar": {
-  //     "app://pubApp3.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp5.gaiamobile.org/manifest.webapp",
-  //     ]
-  //   }
-  // }
-  //
-  // TODO Bug 908999 - Update allowed connections when app gets uninstalled.
-  this._allowedConnections = {};
-
-  // This matrix is used for saving the caller info from the content process,
-  // which is indexed by a random UUID, to know where to return the promise
-  // resolvser's callback when the prompt UI for allowing connections returns.
-  // An example of the object literal is shown as below:
-  //
-  // {
-  //   "fooID": {
-  //     outerWindowID: 12,
-  //     requestID: 34,
-  //     target: pubAppTarget1
-  //   },
-  //   "barID": {
-  //     outerWindowID: 56,
-  //     requestID: 78,
-  //     target: pubAppTarget2
-  //   }
-  // }
-  //
-  // where |outerWindowID| is the ID of the window requesting the connection,
-  //       |requestID| is the ID specifying the promise resolver to return,
-  //       |target| is the target of the process requesting the connection.
-  this._promptUICallers = {};
-
-  // This matrix is used for saving the pair of message ports, which is indexed
-  // by a random UUID, so that each port can know whom it should talk to.
-  // An example of the object literal is shown as below:
-  //
-  // {
-  //   "UUID1": {
-  //     keyword: "keyword1",
-  //     publisher: {
-  //       manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp",
-  //       target: pubAppTarget1,
-  //       pageURL: "app://pubApp1.gaiamobile.org/caller.html",
-  //       messageQueue: [...]
-  //     },
-  //     subscriber: {
-  //       manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp",
-  //       target: subAppTarget1,
-  //       pageURL: "app://pubApp1.gaiamobile.org/handler.html",
-  //       messageQueue: [...]
-  //     }
-  //   },
-  //   "UUID2": {
-  //     keyword: "keyword2",
-  //     publisher: {
-  //       manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp",
-  //       target: pubAppTarget2,
-  //       pageURL: "app://pubApp2.gaiamobile.org/caller.html",
-  //       messageQueue: [...]
-  //     },
-  //     subscriber: {
-  //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
-  //       target: subAppTarget2,
-  //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
-  //       messageQueue: [...]
-  //     }
-  //   }
-  // }
-  this._messagePortPairs = {};
-}
-
-InterAppCommService.prototype = {
+InterAppCommServiceProxy.prototype = {
   registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI,
                                aDescription, aRules) {
-    let manifestURL = aManifestURI.spec;
-    let pageURL = aHandlerPageURI.spec;
-
-    if (DEBUG) {
-      debug("registerConnection: aKeyword: " + aKeyword +
-            " manifestURL: " + manifestURL + " pageURL: " + pageURL +
-            " aDescription: " + aDescription +
-            " aRules.minimumAccessLevel: " + aRules.minimumAccessLevel +
-            " aRules.manifestURLs: " + aRules.manifestURLs +
-            " aRules.installOrigins: " + aRules.installOrigins);
-    }
-
-    let subAppManifestURLs = this._registeredConnections[aKeyword];
-    if (!subAppManifestURLs) {
-      subAppManifestURLs = this._registeredConnections[aKeyword] = {};
-    }
-
-    subAppManifestURLs[manifestURL] = {
-      pageURL: pageURL,
-      description: aDescription,
-      rules: aRules,
-      manifestURL: manifestURL
-    };
-  },
-
-  _matchMinimumAccessLevel: function(aRules, aAppStatus) {
-    if (!aRules || !aRules.minimumAccessLevel) {
-      if (DEBUG) {
-        debug("rules.minimumAccessLevel is not available. No need to match.");
-      }
-      return true;
-    }
-
-    let minAccessLevel = aRules.minimumAccessLevel;
-    switch (minAccessLevel) {
-      case "web":
-        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_INSTALLED ||
-            aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
-            aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-          return true;
-        }
-        break;
-      case "privileged":
-        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
-            aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-          return true;
-        }
-        break;
-      case "certified":
-        if (aAppStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-          return true;
-        }
-        break;
-    }
-
-    if (DEBUG) {
-      debug("rules.minimumAccessLevel is not matched!" +
-            " minAccessLevel: " + minAccessLevel +
-            " aAppStatus : " + aAppStatus);
-    }
-    return false;
-  },
-
-  _matchManifestURLs: function(aRules, aManifestURL) {
-    if (!aRules || !Array.isArray(aRules.manifestURLs)) {
-      if (DEBUG) {
-        debug("rules.manifestURLs is not available. No need to match.");
-      }
-      return true;
-    }
-
-    let manifestURLs = aRules.manifestURLs;
-    if (manifestURLs.indexOf(aManifestURL) != -1) {
-      return true;
-    }
-
-    if (DEBUG) {
-      debug("rules.manifestURLs is not matched!" +
-            " manifestURLs: " + manifestURLs +
-            " aManifestURL : " + aManifestURL);
-    }
-    return false;
-  },
-
-  _matchInstallOrigins: function(aRules, aInstallOrigin) {
-    if (!aRules || !Array.isArray(aRules.installOrigins)) {
-      if (DEBUG) {
-        debug("rules.installOrigins is not available. No need to match.");
-      }
-      return true;
-    }
-
-    let installOrigins = aRules.installOrigins;
-    if (installOrigins.indexOf(aInstallOrigin) != -1) {
-      return true;
-    }
-
-    if (DEBUG) {
-      debug("rules.installOrigins is not matched!" +
-            " installOrigins: " + installOrigins +
-            " installOrigin : " + aInstallOrigin);
-    }
-    return false;
-  },
-
-  _matchRules: function(aPubAppManifestURL, aPubRules,
-                        aSubAppManifestURL, aSubRules) {
-    let pubApp = appsService.getAppByManifestURL(aPubAppManifestURL);
-    let subApp = appsService.getAppByManifestURL(aSubAppManifestURL);
-
-    // TODO Bug 907068 In the initiative step, we only expose this API to
-    // certified apps to meet the time line. Eventually, we need to make
-    // it available for the non-certified apps as well. For now, only the
-    // certified apps can match the rules.
-    if (pubApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED ||
-        subApp.appStatus != Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
-      if (DEBUG) {
-        debug("Only certified apps are allowed to do connections.");
-      }
-      return false;
-    }
-
-    if (!aPubRules && !aSubRules) {
-      if (DEBUG) {
-        debug("No rules for publisher and subscriber. No need to match.");
-      }
-      return true;
-    }
-
-    // Check minimumAccessLevel.
-    if (!this._matchMinimumAccessLevel(aPubRules, subApp.appStatus) ||
-        !this._matchMinimumAccessLevel(aSubRules, pubApp.appStatus)) {
-      return false;
-    }
-
-    // Check manifestURLs.
-    if (!this._matchManifestURLs(aPubRules, aSubAppManifestURL) ||
-        !this._matchManifestURLs(aSubRules, aPubAppManifestURL)) {
-      return false;
-    }
-
-    // Check installOrigins.
-    if (!this._matchInstallOrigins(aPubRules, subApp.installOrigin) ||
-        !this._matchInstallOrigins(aSubRules, pubApp.installOrigin)) {
-      return false;
-    }
-
-    // Check developers.
-    // TODO Do we really want to check this? This one seems naive.
-
-    if (DEBUG) debug("All rules are matched.");
-    return true;
-  },
-
-  _dispatchMessagePorts: function(aKeyword, aPubAppManifestURL,
-                                  aAllowedSubAppManifestURLs,
-                                  aTarget, aOuterWindowID, aRequestID) {
-    if (DEBUG) {
-      debug("_dispatchMessagePorts: aKeyword: " + aKeyword +
-            " aPubAppManifestURL: " + aPubAppManifestURL +
-            " aAllowedSubAppManifestURLs: " + aAllowedSubAppManifestURLs);
-    }
-
-    if (aAllowedSubAppManifestURLs.length == 0) {
-      if (DEBUG) debug("No apps are allowed to connect. Returning.");
-      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
-                               { oid: aOuterWindowID, requestID: aRequestID });
-      return;
-    }
-
-    let subAppManifestURLs = this._registeredConnections[aKeyword];
-    if (!subAppManifestURLs) {
-      if (DEBUG) debug("No apps are subscribed to connect. Returning.");
-      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
-                               { oid: aOuterWindowID, requestID: aRequestID });
-      return;
-    }
-
-    let messagePortIDs = [];
-    aAllowedSubAppManifestURLs.forEach(function(aAllowedSubAppManifestURL) {
-      let subscribedInfo = subAppManifestURLs[aAllowedSubAppManifestURL];
-      if (!subscribedInfo) {
-        if (DEBUG) {
-          debug("The sunscribed info is not available. Skipping: " +
-                aAllowedSubAppManifestURL);
-        }
-        return;
-      }
-
-      // The message port ID is aimed for identifying the coupling targets
-      // to deliver messages with each other. This ID is centrally generated
-      // by the parent and dispatched to both the sender and receiver ends
-      // for creating their own message ports respectively.
-      let messagePortID = UUIDGenerator.generateUUID().toString();
-      this._messagePortPairs[messagePortID] = {
-        keyword: aKeyword,
-        publisher: {
-          manifestURL: aPubAppManifestURL
-        },
-        subscriber: {
-          manifestURL: aAllowedSubAppManifestURL
-        }
-      };
-
-      // Fire system message to deliver the message port to the subscriber.
-      messenger.sendMessage("connection",
-        { keyword: aKeyword,
-          messagePortID: messagePortID },
-        Services.io.newURI(subscribedInfo.pageURL, null, null),
-        Services.io.newURI(subscribedInfo.manifestURL, null, null));
-
-      messagePortIDs.push(messagePortID);
-    }, this);
-
-    if (messagePortIDs.length == 0) {
-      if (DEBUG) debug("No apps are subscribed to connect. Returning.");
-      aTarget.sendAsyncMessage("Webapps:Connect:Return:KO",
-                               { oid: aOuterWindowID, requestID: aRequestID });
-      return;
-    }
-
-    // Return the message port IDs to open the message ports for the publisher.
-    if (DEBUG) debug("messagePortIDs: " + messagePortIDs);
-    aTarget.sendAsyncMessage("Webapps:Connect:Return:OK",
-                             { keyword: aKeyword,
-                               messagePortIDs: messagePortIDs,
-                               oid: aOuterWindowID, requestID: aRequestID });
-  },
-
-  _connect: function(aMessage, aTarget) {
-    let keyword = aMessage.keyword;
-    let pubRules = aMessage.rules;
-    let pubAppManifestURL = aMessage.manifestURL;
-    let outerWindowID = aMessage.outerWindowID;
-    let requestID = aMessage.requestID;
-
-    let subAppManifestURLs = this._registeredConnections[keyword];
-    if (!subAppManifestURLs) {
-      if (DEBUG) {
-        debug("No apps are subscribed for this connection. Returning.");
-      }
-      this._dispatchMessagePorts(keyword, pubAppManifestURL, [],
-                                 aTarget, outerWindowID, requestID);
-      return;
-    }
-
-    // Fetch the apps that used to be allowed to connect before, so that
-    // users don't need to select/allow them again. That is, we only pop up
-    // the prompt UI for the *new* connections.
-    let allowedSubAppManifestURLs = [];
-    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-    if (allowedPubAppManifestURLs &&
-        allowedPubAppManifestURLs[pubAppManifestURL]) {
-      allowedSubAppManifestURLs = allowedPubAppManifestURLs[pubAppManifestURL];
-    }
-
-    // Check rules to see if a subscribed app is allowed to connect.
-    let appsToSelect = [];
-    for (let subAppManifestURL in subAppManifestURLs) {
-      if (allowedSubAppManifestURLs.indexOf(subAppManifestURL) != -1) {
-        if (DEBUG) {
-          debug("Don't need to select again. Skipping: " + subAppManifestURL);
-        }
-        continue;
-      }
-
-      // Only rule-matched publishers/subscribers are allowed to connect.
-      let subscribedInfo = subAppManifestURLs[subAppManifestURL];
-      let subRules = subscribedInfo.rules;
-
-      let matched =
-        this._matchRules(pubAppManifestURL, pubRules,
-                         subAppManifestURL, subRules);
-      if (!matched) {
-        if (DEBUG) {
-          debug("Rules are not matched. Skipping: " + subAppManifestURL);
-        }
-        continue;
-      }
-
-      appsToSelect.push({
-        manifestURL: subAppManifestURL,
-        description: subscribedInfo.description
-      });
-    }
-
-    if (appsToSelect.length == 0) {
-      if (DEBUG) {
-        debug("No additional apps need to be selected for this connection. " +
-              "Just dispatch message ports for the existing connections.");
-      }
-
-      this._dispatchMessagePorts(keyword, pubAppManifestURL,
-                                 allowedSubAppManifestURLs,
-                                 aTarget, outerWindowID, requestID);
-      return;
-    }
-
-    // Remember the caller info with an UUID so that we can know where to
-    // return the promise resolver's callback when the prompt UI returns.
-    let callerID = UUIDGenerator.generateUUID().toString();
-    this._promptUICallers[callerID] = {
-      outerWindowID: outerWindowID,
-      requestID: requestID,
-      target: aTarget
-    };
-
-    // TODO Bug 897169 Temporarily disable the notification for popping up
-    // the prompt until the UX/UI for the prompt is confirmed.
-    //
-    // TODO Bug 908191 We need to change the way of interaction between API and
-    // run-time prompt from observer notification to xpcom-interface caller.
-    //
-    /*
-    if (DEBUG) debug("appsToSelect: " + appsToSelect);
-    Services.obs.notifyObservers(null, "inter-app-comm-select-app",
-      JSON.stringify({ callerID: callerID,
-                       manifestURL: pubAppManifestURL,
-                       keyword: keyword,
-                       appsToSelect: appsToSelect }));
-    */
-
-    // TODO Bug 897169 Simulate the return of the app-selected result by
-    // the prompt, which always allows the connection. This dummy codes
-    // will be removed when the UX/UI for the prompt is ready.
-    if (DEBUG) debug("appsToSelect: " + appsToSelect);
-    Services.obs.notifyObservers(null, 'inter-app-comm-select-app-result',
-      JSON.stringify({ callerID: callerID,
-                       manifestURL: pubAppManifestURL,
-                       keyword: keyword,
-                       selectedApps: appsToSelect }));
-  },
-
-  _getConnections: function(aMessage, aTarget) {
-    let outerWindowID = aMessage.outerWindowID;
-    let requestID = aMessage.requestID;
-
-    let connections = [];
-    for (let keyword in this._allowedConnections) {
-      let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-      for (let allowedPubAppManifestURL in allowedPubAppManifestURLs) {
-        let allowedSubAppManifestURLs =
-          allowedPubAppManifestURLs[allowedPubAppManifestURL];
-        allowedSubAppManifestURLs.forEach(function(allowedSubAppManifestURL) {
-          connections.push({ keyword: keyword,
-                             pubAppManifestURL: allowedPubAppManifestURL,
-                             subAppManifestURL: allowedSubAppManifestURL });
-        });
-      }
-    }
-
-    aTarget.sendAsyncMessage("Webapps:GetConnections:Return:OK",
-                             { connections: connections,
-                               oid: outerWindowID, requestID: requestID });
-  },
-
-  _cancelConnection: function(aMessage) {
-    let keyword = aMessage.keyword;
-    let pubAppManifestURL = aMessage.pubAppManifestURL;
-    let subAppManifestURL = aMessage.subAppManifestURL;
-
-    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-    if (!allowedPubAppManifestURLs) {
-      if (DEBUG) debug("keyword is not found: " + keyword);
-      return;
-    }
-
-    let allowedSubAppManifestURLs =
-      allowedPubAppManifestURLs[pubAppManifestURL];
-    if (!allowedSubAppManifestURLs) {
-      if (DEBUG) debug("publisher is not found: " + pubAppManifestURL);
-      return;
-    }
-
-    let index = allowedSubAppManifestURLs.indexOf(subAppManifestURL);
-    if (index == -1) {
-      if (DEBUG) debug("subscriber is not found: " + subAppManifestURL);
-      return;
-    }
-
-    if (DEBUG) debug("Cancelling the connection.");
-    allowedSubAppManifestURLs.splice(index, 1);
-
-    // Clean up the parent entries if needed.
-    if (allowedSubAppManifestURLs.length == 0) {
-      delete allowedPubAppManifestURLs[pubAppManifestURL];
-      if (Object.keys(allowedPubAppManifestURLs).length == 0) {
-        delete this._allowedConnections[keyword];
-      }
-    }
-
-    if (DEBUG) debug("Unregistering message ports based on this connection.");
-    let messagePortIDs = [];
-    for (let messagePortID in this._messagePortPairs) {
-      let pair = this._messagePortPairs[messagePortID];
-      if (pair.keyword == keyword &&
-          pair.publisher.manifestURL == pubAppManifestURL &&
-          pair.subscriber.manifestURL == subAppManifestURL) {
-        messagePortIDs.push(messagePortID);
-      }
-    }
-    messagePortIDs.forEach(function(aMessagePortID) {
-      delete this._messagePortPairs[aMessagePortID];
-    }, this);
-  },
-
-  _identifyMessagePort: function(aMessagePortID, aManifestURL) {
-    let pair = this._messagePortPairs[aMessagePortID];
-    if (!pair) {
-      if (DEBUG) {
-        debug("Error! The message port ID is invalid: " + aMessagePortID +
-              ", which should have been generated by parent.");
-      }
-      return null;
-    }
-
-    // Check it the message port is for publisher.
-    if (pair.publisher.manifestURL == aManifestURL) {
-      return { pair: pair, isPublisher: true };
-    }
-
-    // Check it the message port is for subscriber.
-    if (pair.subscriber.manifestURL == aManifestURL) {
-      return { pair: pair, isPublisher: false };
-    }
-
-    if (DEBUG) {
-      debug("Error! The manifest URL is invalid: " + aManifestURL +
-            ", which might be a hacked app.");
-    }
-    return null;
-  },
-
-  _registerMessagePort: function(aMessage, aTarget) {
-    let messagePortID = aMessage.messagePortID;
-    let manifestURL = aMessage.manifestURL;
-    let pageURL = aMessage.pageURL;
-
-    let identity = this._identifyMessagePort(messagePortID, manifestURL);
-    if (!identity) {
-      if (DEBUG) {
-        debug("Cannot identify the message port. Failed to register.");
-      }
-      return;
-    }
-
-    if (DEBUG) debug("Registering message port for " + manifestURL);
-    let pair = identity.pair;
-    let isPublisher = identity.isPublisher;
-
-    let sender = isPublisher ? pair.publisher : pair.subscriber;
-    sender.target = aTarget;
-    sender.pageURL = pageURL;
-    sender.messageQueue = [];
-
-    // Check if the other port has queued messages. Deliver them if needed.
-    if (DEBUG) {
-      debug("Checking if the other port used to send messages but queued.");
-    }
-    let receiver = isPublisher ? pair.subscriber : pair.publisher;
-    if (receiver.messageQueue) {
-      while (receiver.messageQueue.length) {
-        let message = receiver.messageQueue.shift();
-        if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
-        sender.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
-                                       { message: message,
-                                         manifestURL: sender.manifestURL,
-                                         pageURL: sender.pageURL,
-                                         messagePortID: messagePortID });
-      }
-    }
-  },
-
-  _unregisterMessagePort: function(aMessage) {
-    let messagePortID = aMessage.messagePortID;
-    let manifestURL = aMessage.manifestURL;
-
-    let identity = this._identifyMessagePort(messagePortID, manifestURL);
-    if (!identity) {
-      if (DEBUG) {
-        debug("Cannot identify the message port. Failed to unregister.");
-      }
-      return;
-    }
-
-    if (DEBUG) {
-      debug("Unregistering message port for " + manifestURL);
-    }
-    delete this._messagePortPairs[messagePortID];
-  },
-
-  _removeTarget: function(aTarget) {
-    if (!aTarget) {
-      if (DEBUG) debug("Error! aTarget cannot be null/undefined in any way.");
-      return
-    }
-
-    if (DEBUG) debug("Unregistering message ports based on this target.");
-    let messagePortIDs = [];
-    for (let messagePortID in this._messagePortPairs) {
-      let pair = this._messagePortPairs[messagePortID];
-      if (pair.publisher.target === aTarget ||
-          pair.subscriber.target === aTarget) {
-        messagePortIDs.push(messagePortID);
-      }
-    }
-    messagePortIDs.forEach(function(aMessagePortID) {
-      delete this._messagePortPairs[aMessagePortID];
-    }, this);
-  },
-
-  _postMessage: function(aMessage) {
-    let messagePortID = aMessage.messagePortID;
-    let manifestURL = aMessage.manifestURL;
-    let message = aMessage.message;
-
-    let identity = this._identifyMessagePort(messagePortID, manifestURL);
-    if (!identity) {
-      if (DEBUG) debug("Cannot identify the message port. Failed to post.");
-      return;
-    }
-
-    let pair = identity.pair;
-    let isPublisher = identity.isPublisher;
-
-    let receiver = isPublisher ? pair.subscriber : pair.publisher;
-    if (!receiver.target) {
-      if (DEBUG) {
-        debug("The receiver's target is not ready yet. Queuing the message.");
-      }
-      let sender = isPublisher ? pair.publisher : pair.subscriber;
-      sender.messageQueue.push(message);
-      return;
-    }
-
-    if (DEBUG) debug("Delivering message: " + JSON.stringify(message));
-    receiver.target.sendAsyncMessage("InterAppMessagePort:OnMessage",
-                                     { manifestURL: receiver.manifestURL,
-                                       pageURL: receiver.pageURL,
-                                       messagePortID: messagePortID,
-                                       message: message });
-  },
-
-  _handleSelectcedApps: function(aData) {
-    let callerID = aData.callerID;
-    let caller = this._promptUICallers[callerID];
-    if (!caller) {
-      if (DEBUG) debug("Error! Cannot find the caller.");
-      return;
-    }
-
-    delete this._promptUICallers[callerID];
-
-    let outerWindowID = caller.outerWindowID;
-    let requestID = caller.requestID;
-    let target = caller.target;
-
-    let manifestURL = aData.manifestURL;
-    let keyword = aData.keyword;
-    let selectedApps = aData.selectedApps;
-
-    if (selectedApps.length == 0) {
-      if (DEBUG) debug("No apps are selected to connect.")
-      this._dispatchMessagePorts(keyword, manifestURL, [],
-                                 target, outerWindowID, requestID);
-      return;
-    }
-
-    // Find the entry of allowed connections to add the selected apps.
-    let allowedPubAppManifestURLs = this._allowedConnections[keyword];
-    if (!allowedPubAppManifestURLs) {
-      allowedPubAppManifestURLs = this._allowedConnections[keyword] = {};
-    }
-    let allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL];
-    if (!allowedSubAppManifestURLs) {
-      allowedSubAppManifestURLs = allowedPubAppManifestURLs[manifestURL] = [];
-    }
-
-    // Add the selected app into the existing set of allowed connections.
-    selectedApps.forEach(function(aSelectedApp) {
-      let allowedSubAppManifestURL = aSelectedApp.manifestURL;
-      if (allowedSubAppManifestURLs.indexOf(allowedSubAppManifestURL) == -1) {
-        allowedSubAppManifestURLs.push(allowedSubAppManifestURL);
-      }
-    });
-
-    // Finally, dispatch the message ports for the allowed connections,
-    // including the old connections and the newly selected connection.
-    this._dispatchMessagePorts(keyword, manifestURL, allowedSubAppManifestURLs,
-                               target, outerWindowID, requestID);
-  },
-
-  receiveMessage: function(aMessage) {
-    if (DEBUG) debug("receiveMessage: name: " + aMessage.name);
-    let message = aMessage.json;
-    let target = aMessage.target;
-
-    // To prevent the hacked child process from sending commands to parent
-    // to do illegal connections, we need to check its manifest URL.
-    if (aMessage.name !== "child-process-shutdown" &&
-        // TODO: fix bug 988142 to re-enable "InterAppMessagePort:Unregister".
-        aMessage.name !== "InterAppMessagePort:Unregister" &&
-        kMessages.indexOf(aMessage.name) != -1) {
-      if (!target.assertContainApp(message.manifestURL)) {
-        if (DEBUG) {
-          debug("Got message from a process carrying illegal manifest URL.");
-        }
-        return null;
-      }
-    }
-
-    switch (aMessage.name) {
-      case "Webapps:Connect":
-        this._connect(message, target);
-        break;
-      case "Webapps:GetConnections":
-        this._getConnections(message, target);
-        break;
-      case "InterAppConnection:Cancel":
-        this._cancelConnection(message);
-        break;
-      case "InterAppMessagePort:PostMessage":
-        this._postMessage(message);
-        break;
-      case "InterAppMessagePort:Register":
-        this._registerMessagePort(message, target);
-        break;
-      case "InterAppMessagePort:Unregister":
-        this._unregisterMessagePort(message);
-        break;
-      case "child-process-shutdown":
-        this._removeTarget(target);
-        break;
-    }
-  },
-
-  observe: function(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "xpcom-shutdown":
-        Services.obs.removeObserver(this, "xpcom-shutdown");
-        Services.obs.removeObserver(this, "inter-app-comm-select-app-result");
-        kMessages.forEach(function(aMsg) {
-          ppmm.removeMessageListener(aMsg, this);
-        }, this);
-        ppmm = null;
-        break;
-      case "inter-app-comm-select-app-result":
-        if (DEBUG) debug("inter-app-comm-select-app-result: " + aData);
-        this._handleSelectcedApps(JSON.parse(aData));
-        break;
-    }
+    InterAppCommService.
+      registerConnection(aKeyword, aHandlerPageURI, aManifestURI,
+                         aDescription, aRules);
   },
 
   classID: Components.ID("{3dd15ce6-e7be-11e2-82bc-77967e7a63e6}"),
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterAppCommService,
-                                         Ci.nsIObserver])
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterAppCommService])
 }
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppCommService]);
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppCommServiceProxy]);
copy from dom/apps/src/InterAppCommService.js
copy to dom/apps/src/InterAppCommService.jsm
--- a/dom/apps/src/InterAppCommService.js
+++ b/dom/apps/src/InterAppCommService.jsm
@@ -1,16 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
+this.EXPORTED_SYMBOLS = ["InterAppCommService"];
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 const DEBUG = false;
 function debug(aMsg) {
   dump("-- InterAppCommService: " + Date.now() + ": " + aMsg + "\n");
 }
@@ -34,187 +36,205 @@ XPCOMUtils.defineLazyServiceGetter(this,
 const kMessages =["Webapps:Connect",
                   "Webapps:GetConnections",
                   "InterAppConnection:Cancel",
                   "InterAppMessagePort:PostMessage",
                   "InterAppMessagePort:Register",
                   "InterAppMessagePort:Unregister",
                   "child-process-shutdown"];
 
-function InterAppCommService() {
-  Services.obs.addObserver(this, "xpcom-shutdown", false);
-  Services.obs.addObserver(this, "inter-app-comm-select-app-result", false);
+/**
+ * This module contains helpers for Inter-App Communication API [1] related
+ * purposes, which plays the role of the central service receiving messages
+ * from and interacting with the content processes.
+ *
+ * [1] https://wiki.mozilla.org/WebAPI/Inter_App_Communication_Alt_proposal
+ */
 
-  kMessages.forEach(function(aMsg) {
-    ppmm.addMessageListener(aMsg, this);
-  }, this);
+this.InterAppCommService = {
+  init: function() {
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+    Services.obs.addObserver(this, "inter-app-comm-select-app-result", false);
+
+    kMessages.forEach(function(aMsg) {
+      ppmm.addMessageListener(aMsg, this);
+    }, this);
 
-  // This matrix is used for saving the inter-app connection info registered in
-  // the app manifest. The object literal is defined as below:
-  //
-  // {
-  //   "keyword1": {
-  //     "subAppManifestURL1": {
-  //       /* subscribed info */
-  //     },
-  //     "subAppManifestURL2": {
-  //       /* subscribed info */
-  //     },
-  //     ...
-  //   },
-  //   "keyword2": {
-  //     "subAppManifestURL3": {
-  //       /* subscribed info */
-  //     },
-  //     ...
-  //   },
-  //   ...
-  // }
-  //
-  // For example:
-  //
-  // {
-  //   "foo": {
-  //     "app://subApp1.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp1.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     },
-  //     "app://subApp2.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp2.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     }
-  //   },
-  //   "bar": {
-  //     "app://subApp3.gaiamobile.org/manifest.webapp": {
-  //       pageURL: "app://subApp3.gaiamobile.org/handler.html",
-  //       description: "blah blah",
-  //       rules: { ... }
-  //     }
-  //   }
-  // }
-  //
-  // TODO Bug 908999 - Update registered connections when app gets uninstalled.
-  this._registeredConnections = {};
+    // This matrix is used for saving the inter-app connection info registered in
+    // the app manifest. The object literal is defined as below:
+    //
+    // {
+    //   "keyword1": {
+    //     "subAppManifestURL1": {
+    //       /* subscribed info */
+    //     },
+    //     "subAppManifestURL2": {
+    //       /* subscribed info */
+    //     },
+    //     ...
+    //   },
+    //   "keyword2": {
+    //     "subAppManifestURL3": {
+    //       /* subscribed info */
+    //     },
+    //     ...
+    //   },
+    //   ...
+    // }
+    //
+    // For example:
+    //
+    // {
+    //   "foo": {
+    //     "app://subApp1.gaiamobile.org/manifest.webapp": {
+    //       pageURL: "app://subApp1.gaiamobile.org/handler.html",
+    //       description: "blah blah",
+    //       rules: { ... }
+    //     },
+    //     "app://subApp2.gaiamobile.org/manifest.webapp": {
+    //       pageURL: "app://subApp2.gaiamobile.org/handler.html",
+    //       description: "blah blah",
+    //       rules: { ... }
+    //     }
+    //   },
+    //   "bar": {
+    //     "app://subApp3.gaiamobile.org/manifest.webapp": {
+    //       pageURL: "app://subApp3.gaiamobile.org/handler.html",
+    //       description: "blah blah",
+    //       rules: { ... }
+    //     }
+    //   }
+    // }
+    //
+    // TODO Bug 908999 - Update registered connections when app gets uninstalled.
+    this._registeredConnections = {};
 
-  // This matrix is used for saving the permitted connections, which allows
-  // the messaging between publishers and subscribers. The object literal is
-  // defined as below:
-  //
-  // {
-  //   "keyword1": {
-  //     "pubAppManifestURL1": [
-  //       "subAppManifestURL1",
-  //       "subAppManifestURL2",
-  //       ...
-  //     ],
-  //     "pubAppManifestURL2": [
-  //       "subAppManifestURL3",
-  //       "subAppManifestURL4",
-  //       ...
-  //     ],
-  //     ...
-  //   },
-  //   "keyword2": {
-  //     "pubAppManifestURL3": [
-  //       "subAppManifestURL5",
-  //       ...
-  //     ],
-  //     ...
-  //   },
-  //   ...
-  // }
-  //
-  // For example:
-  //
-  // {
-  //   "foo": {
-  //     "app://pubApp1.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp1.gaiamobile.org/manifest.webapp",
-  //       "app://subApp2.gaiamobile.org/manifest.webapp"
-  //     ],
-  //     "app://pubApp2.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp3.gaiamobile.org/manifest.webapp",
-  //       "app://subApp4.gaiamobile.org/manifest.webapp"
-  //     ]
-  //   },
-  //   "bar": {
-  //     "app://pubApp3.gaiamobile.org/manifest.webapp": [
-  //       "app://subApp5.gaiamobile.org/manifest.webapp",
-  //     ]
-  //   }
-  // }
-  //
-  // TODO Bug 908999 - Update allowed connections when app gets uninstalled.
-  this._allowedConnections = {};
+    // This matrix is used for saving the permitted connections, which allows
+    // the messaging between publishers and subscribers. The object literal is
+    // defined as below:
+    //
+    // {
+    //   "keyword1": {
+    //     "pubAppManifestURL1": [
+    //       "subAppManifestURL1",
+    //       "subAppManifestURL2",
+    //       ...
+    //     ],
+    //     "pubAppManifestURL2": [
+    //       "subAppManifestURL3",
+    //       "subAppManifestURL4",
+    //       ...
+    //     ],
+    //     ...
+    //   },
+    //   "keyword2": {
+    //     "pubAppManifestURL3": [
+    //       "subAppManifestURL5",
+    //       ...
+    //     ],
+    //     ...
+    //   },
+    //   ...
+    // }
+    //
+    // For example:
+    //
+    // {
+    //   "foo": {
+    //     "app://pubApp1.gaiamobile.org/manifest.webapp": [
+    //       "app://subApp1.gaiamobile.org/manifest.webapp",
+    //       "app://subApp2.gaiamobile.org/manifest.webapp"
+    //     ],
+    //     "app://pubApp2.gaiamobile.org/manifest.webapp": [
+    //       "app://subApp3.gaiamobile.org/manifest.webapp",
+    //       "app://subApp4.gaiamobile.org/manifest.webapp"
+    //     ]
+    //   },
+    //   "bar": {
+    //     "app://pubApp3.gaiamobile.org/manifest.webapp": [
+    //       "app://subApp5.gaiamobile.org/manifest.webapp",
+    //     ]
+    //   }
+    // }
+    //
+    // TODO Bug 908999 - Update allowed connections when app gets uninstalled.
+    this._allowedConnections = {};
 
-  // This matrix is used for saving the caller info from the content process,
-  // which is indexed by a random UUID, to know where to return the promise
-  // resolvser's callback when the prompt UI for allowing connections returns.
-  // An example of the object literal is shown as below:
-  //
-  // {
-  //   "fooID": {
-  //     outerWindowID: 12,
-  //     requestID: 34,
-  //     target: pubAppTarget1
-  //   },
-  //   "barID": {
-  //     outerWindowID: 56,
-  //     requestID: 78,
-  //     target: pubAppTarget2
-  //   }
-  // }
-  //
-  // where |outerWindowID| is the ID of the window requesting the connection,
-  //       |requestID| is the ID specifying the promise resolver to return,
-  //       |target| is the target of the process requesting the connection.
-  this._promptUICallers = {};
+    // This matrix is used for saving the caller info from the content process,
+    // which is indexed by a random UUID, to know where to return the promise
+    // resolvser's callback when the prompt UI for allowing connections returns.
+    // An example of the object literal is shown as below:
+    //
+    // {
+    //   "fooID": {
+    //     outerWindowID: 12,
+    //     requestID: 34,
+    //     target: pubAppTarget1
+    //   },
+    //   "barID": {
+    //     outerWindowID: 56,
+    //     requestID: 78,
+    //     target: pubAppTarget2
+    //   }
+    // }
+    //
+    // where |outerWindowID| is the ID of the window requesting the connection,
+    //       |requestID| is the ID specifying the promise resolver to return,
+    //       |target| is the target of the process requesting the connection.
+    this._promptUICallers = {};
 
-  // This matrix is used for saving the pair of message ports, which is indexed
-  // by a random UUID, so that each port can know whom it should talk to.
-  // An example of the object literal is shown as below:
-  //
-  // {
-  //   "UUID1": {
-  //     keyword: "keyword1",
-  //     publisher: {
-  //       manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp",
-  //       target: pubAppTarget1,
-  //       pageURL: "app://pubApp1.gaiamobile.org/caller.html",
-  //       messageQueue: [...]
-  //     },
-  //     subscriber: {
-  //       manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp",
-  //       target: subAppTarget1,
-  //       pageURL: "app://pubApp1.gaiamobile.org/handler.html",
-  //       messageQueue: [...]
-  //     }
-  //   },
-  //   "UUID2": {
-  //     keyword: "keyword2",
-  //     publisher: {
-  //       manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp",
-  //       target: pubAppTarget2,
-  //       pageURL: "app://pubApp2.gaiamobile.org/caller.html",
-  //       messageQueue: [...]
-  //     },
-  //     subscriber: {
-  //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
-  //       target: subAppTarget2,
-  //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
-  //       messageQueue: [...]
-  //     }
-  //   }
-  // }
-  this._messagePortPairs = {};
-}
+    // This matrix is used for saving the pair of message ports, which is indexed
+    // by a random UUID, so that each port can know whom it should talk to.
+    // An example of the object literal is shown as below:
+    //
+    // {
+    //   "UUID1": {
+    //     keyword: "keyword1",
+    //     publisher: {
+    //       manifestURL: "app://pubApp1.gaiamobile.org/manifest.webapp",
+    //       target: pubAppTarget1,
+    //       pageURL: "app://pubApp1.gaiamobile.org/caller.html",
+    //       messageQueue: [...]
+    //     },
+    //     subscriber: {
+    //       manifestURL: "app://subApp1.gaiamobile.org/manifest.webapp",
+    //       target: subAppTarget1,
+    //       pageURL: "app://pubApp1.gaiamobile.org/handler.html",
+    //       messageQueue: [...]
+    //     }
+    //   },
+    //   "UUID2": {
+    //     keyword: "keyword2",
+    //     publisher: {
+    //       manifestURL: "app://pubApp2.gaiamobile.org/manifest.webapp",
+    //       target: pubAppTarget2,
+    //       pageURL: "app://pubApp2.gaiamobile.org/caller.html",
+    //       messageQueue: [...]
+    //     },
+    //     subscriber: {
+    //       manifestURL: "app://subApp2.gaiamobile.org/manifest.webapp",
+    //       target: subAppTarget2,
+    //       pageURL: "app://pubApp2.gaiamobile.org/handler.html",
+    //       messageQueue: [...]
+    //     }
+    //   }
+    // }
+    this._messagePortPairs = {};
+  },
 
-InterAppCommService.prototype = {
+  /**
+   * Registration of a page that wants to be connected to other apps through
+   * the Inter-App Communication API.
+   *
+   * @param aKeyword        The connection's keyword.
+   * @param aHandlerPageURI The URI of the handler's page.
+   * @param aManifestURI    The webapp's manifest URI.
+   * @param aDescription    The connection's description.
+   * @param aRules          The connection's rules.
+   */
   registerConnection: function(aKeyword, aHandlerPageURI, aManifestURI,
                                aDescription, aRules) {
     let manifestURL = aManifestURI.spec;
     let pageURL = aHandlerPageURI.spec;
 
     if (DEBUG) {
       debug("registerConnection: aKeyword: " + aKeyword +
             " manifestURL: " + manifestURL + " pageURL: " + pageURL +
@@ -858,17 +878,12 @@ InterAppCommService.prototype = {
         }, this);
         ppmm = null;
         break;
       case "inter-app-comm-select-app-result":
         if (DEBUG) debug("inter-app-comm-select-app-result: " + aData);
         this._handleSelectcedApps(JSON.parse(aData));
         break;
     }
-  },
-
-  classID: Components.ID("{3dd15ce6-e7be-11e2-82bc-77967e7a63e6}"),
+  }
+};
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterAppCommService,
-                                         Ci.nsIObserver])
-}
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([InterAppCommService]);
+InterAppCommService.init();
--- a/dom/apps/src/moz.build
+++ b/dom/apps/src/moz.build
@@ -22,16 +22,17 @@ EXTRA_COMPONENTS += [
     'Webapps.js',
     'Webapps.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'AppDownloadManager.jsm',
     'AppsServiceChild.jsm',
     'FreeSpaceWatcher.jsm',
+    'InterAppCommService.jsm',
     'OfflineCacheInstaller.jsm',
     'PermissionsInstaller.jsm',
     'PermissionsTable.jsm',
     'StoreTrustAnchor.jsm',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'AppsUtils.jsm',
new file mode 100644
--- /dev/null
+++ b/dom/apps/tests/unit/test_inter_app_comm_service.js
@@ -0,0 +1,457 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/InterAppCommService.jsm");
+
+let UUIDGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                      .getService(Ci.nsIUUIDGenerator);
+
+const MESSAGE_PORT_ID = UUIDGenerator.generateUUID().toString();
+const FAKE_MESSAGE_PORT_ID = UUIDGenerator.generateUUID().toString();
+const OUTER_WINDOW_ID = UUIDGenerator.generateUUID().toString();
+const REQUEST_ID = UUIDGenerator.generateUUID().toString();
+
+const PUB_APP_MANIFEST_URL = "app://pubApp.gaiamobile.org/manifest.webapp";
+const SUB_APP_MANIFEST_URL = "app://subApp.gaiamobile.org/manifest.webapp";
+
+const PUB_APP_PAGE_URL = "app://pubApp.gaiamobile.org/handler.html";
+const SUB_APP_PAGE_URL = "app://subApp.gaiamobile.org/handler.html";
+
+const KEYWORD = "test";
+
+function create_message_port_pair(aMessagePortId,
+                                  aKeyword,
+                                  aPubManifestURL,
+                                  aSubManifestURL) {
+  InterAppCommService._messagePortPairs[aMessagePortId] = {
+    keyword: aKeyword,
+    publisher: {
+      manifestURL: aPubManifestURL
+    },
+    subscriber: {
+      manifestURL: aSubManifestURL
+    }
+  };
+}
+
+function clear_message_port_pairs() {
+  InterAppCommService._messagePortPairs = {};
+}
+
+function register_message_port(aMessagePortId,
+                               aManifestURL,
+                               aPageURL,
+                               aTargetSendAsyncMessage) {
+  let message = {
+    name: "InterAppMessagePort:Register",
+    json: {
+      messagePortID: aMessagePortId,
+      manifestURL: aManifestURL,
+      pageURL: aPageURL
+    },
+    target: {
+      sendAsyncMessage: function(aName, aData) {
+        if (aTargetSendAsyncMessage) {
+          aTargetSendAsyncMessage(aName, aData);
+        }
+      },
+      assertContainApp: function(_manifestURL) {
+        return (aManifestURL == _manifestURL);
+      }
+    }
+  };
+
+  InterAppCommService.receiveMessage(message);
+
+  return message.target;
+}
+
+function register_message_ports(aMessagePortId,
+                                aPubTargetSendAsyncMessage,
+                                aSubTargetSendAsyncMessage) {
+  let pubTarget = register_message_port(aMessagePortId,
+                                        PUB_APP_MANIFEST_URL,
+                                        PUB_APP_PAGE_URL,
+                                        aPubTargetSendAsyncMessage);
+
+  let subTarget = register_message_port(aMessagePortId,
+                                        SUB_APP_MANIFEST_URL,
+                                        SUB_APP_PAGE_URL,
+                                        aSubTargetSendAsyncMessage);
+
+  return { pubTarget: pubTarget, subTarget: subTarget };
+}
+
+function unregister_message_port(aMessagePortId,
+                                 aManifestURL) {
+  let message = {
+    name: "InterAppMessagePort:Unregister",
+    json: {
+      messagePortID: aMessagePortId,
+      manifestURL: aManifestURL
+    },
+    target: {
+      assertContainApp: function(_manifestURL) {
+        return (aManifestURL == _manifestURL);
+      }
+    }
+  };
+
+  InterAppCommService.receiveMessage(message);
+}
+
+function remove_target(aTarget) {
+  let message = {
+    name: "child-process-shutdown",
+    target: aTarget
+  };
+
+  InterAppCommService.receiveMessage(message);
+}
+
+function post_message(aMessagePortId,
+                      aManifestURL,
+                      aMessage) {
+  let message = {
+    name: "InterAppMessagePort:PostMessage",
+    json: {
+      messagePortID: aMessagePortId,
+      manifestURL: aManifestURL,
+      message: aMessage
+    },
+    target: {
+      assertContainApp: function(_manifestURL) {
+        return (aManifestURL == _manifestURL);
+      }
+    }
+  };
+
+  InterAppCommService.receiveMessage(message);
+}
+
+function create_allowed_connections(aKeyword,
+                                    aPubManifestURL,
+                                    aSubManifestURL) {
+  let allowedPubAppManifestURLs =
+    InterAppCommService._allowedConnections[aKeyword] = {};
+
+  allowedPubAppManifestURLs[aPubManifestURL] = [aSubManifestURL];
+}
+
+function clear_allowed_connections() {
+  InterAppCommService._allowedConnections = {};
+}
+
+function get_connections(aManifestURL,
+                         aOuterWindowID,
+                         aRequestID,
+                         aTargetSendAsyncMessage) {
+  let message = {
+    name: "Webapps:GetConnections",
+    json: {
+      manifestURL: aManifestURL,
+      outerWindowID: aOuterWindowID,
+      requestID: aRequestID
+    },
+    target: {
+      sendAsyncMessage: function(aName, aData) {
+        if (aTargetSendAsyncMessage) {
+          aTargetSendAsyncMessage(aName, aData);
+        }
+      },
+      assertContainApp: function(_manifestURL) {
+        return (aManifestURL == _manifestURL);
+      }
+    }
+  };
+
+  InterAppCommService.receiveMessage(message);
+}
+
+function cancel_connections(aManifestURL,
+                            aKeyword,
+                            aPubManifestURL,
+                            aSubManifestURL) {
+  let message = {
+    name: "InterAppConnection:Cancel",
+    json: {
+      manifestURL: aManifestURL,
+      keyword: aKeyword,
+      pubAppManifestURL: aPubManifestURL,
+      subAppManifestURL: aSubManifestURL
+    },
+    target: {
+      assertContainApp: function(_manifestURL) {
+        return (aManifestURL == _manifestURL);
+      }
+    }
+  };
+
+  InterAppCommService.receiveMessage(message);
+}
+
+add_test(function test_registerMessagePort() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  let targets = register_message_ports(MESSAGE_PORT_ID);
+
+  let messagePortPair = InterAppCommService._messagePortPairs[MESSAGE_PORT_ID];
+
+  do_check_eq(PUB_APP_PAGE_URL, messagePortPair.publisher.pageURL);
+  do_check_eq(SUB_APP_PAGE_URL, messagePortPair.subscriber.pageURL);
+
+  do_check_true(targets.pubTarget === messagePortPair.publisher.target);
+  do_check_true(targets.subTarget === messagePortPair.subscriber.target);
+
+  clear_message_port_pairs();
+  run_next_test();
+});
+
+add_test(function test_failToRegisterMessagePort() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  let targets = register_message_ports(FAKE_MESSAGE_PORT_ID);
+
+  let messagePortPair = InterAppCommService._messagePortPairs[MESSAGE_PORT_ID];
+
+  // Because it failed to register, the page URLs and targets don't exist.
+  do_check_true(messagePortPair.publisher.pageURL === undefined);
+  do_check_true(messagePortPair.subscriber.pageURL === undefined);
+
+  do_check_true(messagePortPair.publisher.target === undefined);
+  do_check_true(messagePortPair.subscriber.target === undefined);
+
+  clear_message_port_pairs();
+  run_next_test();
+});
+
+add_test(function test_unregisterMessagePort() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  register_message_ports(MESSAGE_PORT_ID);
+
+  unregister_message_port(MESSAGE_PORT_ID, PUB_APP_MANIFEST_URL);
+
+  do_check_true(InterAppCommService._messagePortPairs[MESSAGE_PORT_ID]
+                === undefined);
+
+  clear_message_port_pairs();
+  run_next_test();
+});
+
+add_test(function test_failToUnregisterMessagePort() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  register_message_ports(MESSAGE_PORT_ID);
+
+  unregister_message_port(FAKE_MESSAGE_PORT_ID, PUB_APP_MANIFEST_URL);
+
+  // Because it failed to unregister, the entry still exists.
+  do_check_true(InterAppCommService._messagePortPairs[MESSAGE_PORT_ID]
+                !== undefined);
+
+  clear_message_port_pairs();
+  run_next_test();
+});
+
+add_test(function test_removeTarget() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  let targets = register_message_ports(MESSAGE_PORT_ID);
+
+  remove_target(targets.pubTarget);
+
+  do_check_true(InterAppCommService._messagePortPairs[MESSAGE_PORT_ID]
+                === undefined);
+
+  clear_message_port_pairs();
+  run_next_test();
+});
+
+add_test(function test_postMessage() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  let countPubAppOnMessage = 0;
+  function pubAppOnMessage(aName, aData) {
+    countPubAppOnMessage++;
+
+    do_check_eq(aName, "InterAppMessagePort:OnMessage");
+    do_check_eq(aData.manifestURL, PUB_APP_MANIFEST_URL);
+    do_check_eq(aData.pageURL, PUB_APP_PAGE_URL);
+    do_check_eq(aData.messagePortID, MESSAGE_PORT_ID);
+
+    if (countPubAppOnMessage == 1) {
+      do_check_eq(aData.message.text, "sub app says world");
+
+      post_message(MESSAGE_PORT_ID,
+                   PUB_APP_MANIFEST_URL,
+                   { text: "pub app says hello again" });
+
+    } else if (countPubAppOnMessage == 2) {
+      do_check_eq(aData.message.text, "sub app says world again");
+
+      clear_message_port_pairs();
+      run_next_test();
+    } else {
+      do_throw("pub app receives an unexpected message")
+    }
+  };
+
+  let countSubAppOnMessage = 0;
+  function subAppOnMessage(aName, aData) {
+    countSubAppOnMessage++;
+
+    do_check_eq(aName, "InterAppMessagePort:OnMessage");
+    do_check_eq(aData.manifestURL, SUB_APP_MANIFEST_URL);
+    do_check_eq(aData.pageURL, SUB_APP_PAGE_URL);
+    do_check_eq(aData.messagePortID, MESSAGE_PORT_ID);
+
+    if (countSubAppOnMessage == 1) {
+      do_check_eq(aData.message.text, "pub app says hello");
+
+      post_message(MESSAGE_PORT_ID,
+                   SUB_APP_MANIFEST_URL,
+                   { text: "sub app says world" });
+
+    } else if (countSubAppOnMessage == 2) {
+      do_check_eq(aData.message.text, "pub app says hello again");
+
+      post_message(MESSAGE_PORT_ID,
+                   SUB_APP_MANIFEST_URL,
+                   { text: "sub app says world again" });
+    } else {
+      do_throw("sub app receives an unexpected message")
+    }
+  };
+
+  register_message_ports(MESSAGE_PORT_ID, pubAppOnMessage, subAppOnMessage);
+
+  post_message(MESSAGE_PORT_ID,
+               PUB_APP_MANIFEST_URL,
+               { text: "pub app says hello" });
+});
+
+add_test(function test_registerMessagePort_with_queued_messages() {
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  register_message_port(MESSAGE_PORT_ID,
+                        PUB_APP_MANIFEST_URL,
+                        PUB_APP_PAGE_URL);
+
+  post_message(MESSAGE_PORT_ID,
+               PUB_APP_MANIFEST_URL,
+               { text: "pub app says hello" });
+
+  post_message(MESSAGE_PORT_ID,
+               PUB_APP_MANIFEST_URL,
+               { text: "pub app says hello again" });
+
+  let countSubAppOnMessage = 0;
+  function subAppOnMessage(aName, aData) {
+    countSubAppOnMessage++;
+
+    do_check_eq(aName, "InterAppMessagePort:OnMessage");
+    do_check_eq(aData.manifestURL, SUB_APP_MANIFEST_URL);
+    do_check_eq(aData.pageURL, SUB_APP_PAGE_URL);
+    do_check_eq(aData.messagePortID, MESSAGE_PORT_ID);
+
+    if (countSubAppOnMessage == 1) {
+      do_check_eq(aData.message.text, "pub app says hello");
+    } else if (countSubAppOnMessage == 2) {
+      do_check_eq(aData.message.text, "pub app says hello again");
+
+      clear_message_port_pairs();
+      run_next_test();
+    } else {
+      do_throw("sub app receives an unexpected message")
+    }
+  };
+
+  register_message_port(MESSAGE_PORT_ID,
+                        SUB_APP_MANIFEST_URL,
+                        SUB_APP_PAGE_URL,
+                        subAppOnMessage);
+});
+
+add_test(function test_getConnections() {
+  create_allowed_connections(KEYWORD,
+                             PUB_APP_MANIFEST_URL,
+                             SUB_APP_MANIFEST_URL);
+
+  function onGetConnections(aName, aData) {
+    do_check_eq(aName, "Webapps:GetConnections:Return:OK");
+    do_check_eq(aData.oid, OUTER_WINDOW_ID);
+    do_check_eq(aData.requestID, REQUEST_ID);
+
+    let connections = aData.connections;
+    do_check_eq(connections.length, 1);
+    do_check_eq(connections[0].keyword, KEYWORD);
+    do_check_eq(connections[0].pubAppManifestURL, PUB_APP_MANIFEST_URL);
+    do_check_eq(connections[0].subAppManifestURL, SUB_APP_MANIFEST_URL);
+
+    clear_allowed_connections();
+    run_next_test();
+  };
+
+  get_connections(PUB_APP_MANIFEST_URL,
+                  OUTER_WINDOW_ID,
+                  REQUEST_ID,
+                  onGetConnections);
+});
+
+add_test(function test_cancelConnection() {
+  create_allowed_connections(KEYWORD,
+                             PUB_APP_MANIFEST_URL,
+                             SUB_APP_MANIFEST_URL);
+
+  create_message_port_pair(MESSAGE_PORT_ID,
+                           KEYWORD,
+                           PUB_APP_MANIFEST_URL,
+                           SUB_APP_MANIFEST_URL);
+
+  register_message_ports(MESSAGE_PORT_ID);
+
+  cancel_connections(PUB_APP_MANIFEST_URL,
+                     KEYWORD,
+                     PUB_APP_MANIFEST_URL,
+                     SUB_APP_MANIFEST_URL);
+
+  do_check_true(InterAppCommService._allowedConnections[KEYWORD]
+                === undefined);
+
+  do_check_true(InterAppCommService._messagePortPairs[MESSAGE_PORT_ID]
+                === undefined);
+
+  clear_allowed_connections();
+  clear_message_port_pairs();
+  run_next_test();
+});
+
+function run_test() {
+  do_get_profile();
+
+  run_next_test();
+}
--- a/dom/apps/tests/unit/xpcshell.ini
+++ b/dom/apps/tests/unit/xpcshell.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 head =
 tail =
 
+[test_inter_app_comm_service.js]
 [test_manifestSanitizer.js]
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -1624,68 +1624,58 @@ int64_t GetPluginLastModifiedTime(const 
   localfile->GetLastModifiedTime(&fileModTime);
 #endif
 
   return fileModTime;
 }
 
 bool
 GetPluginIsFromExtension(const nsCOMPtr<nsIFile>& pluginFile,
-                         const nsCOMPtr<nsISimpleEnumerator>& extensionDirs)
+                         const nsCOMArray<nsIFile>& extensionDirs)
 {
-  if (!extensionDirs) {
-    return false;
-  }
-
-  bool hasMore;
-  while (NS_SUCCEEDED(extensionDirs->HasMoreElements(&hasMore)) && hasMore) {
-    nsCOMPtr<nsISupports> supports;
-    nsresult rv = extensionDirs->GetNext(getter_AddRefs(supports));
-    if (NS_FAILED(rv)) {
-      continue;
-    }
-
-    nsCOMPtr<nsIFile> extDir(do_QueryInterface(supports, &rv));
-    if (NS_FAILED(rv)) {
-      continue;
-    }
-
-    nsCOMPtr<nsIFile> dir;
-    if (NS_FAILED(extDir->Clone(getter_AddRefs(dir)))) {
-      continue;
-    }
-
+  for (uint32_t i = 0; i < extensionDirs.Length(); ++i) {
     bool contains;
-    if (NS_FAILED(dir->Contains(pluginFile, true, &contains)) || !contains) {
+    if (NS_FAILED(extensionDirs[i]->Contains(pluginFile, true, &contains)) || !contains) {
       continue;
     }
 
     return true;
   }
 
   return false;
 }
 
-nsCOMPtr<nsISimpleEnumerator>
-GetExtensionDirectories()
+void
+GetExtensionDirectories(nsCOMArray<nsIFile>& dirs)
 {
   nsCOMPtr<nsIProperties> dirService = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
   if (!dirService) {
-    return nullptr;
+    return;
   }
 
   nsCOMPtr<nsISimpleEnumerator> list;
   nsresult rv = dirService->Get(XRE_EXTENSIONS_DIR_LIST,
                                 NS_GET_IID(nsISimpleEnumerator),
                                 getter_AddRefs(list));
   if (NS_FAILED(rv)) {
-    return nullptr;
+    return;
   }
 
-  return list;
+  bool more;
+  while (NS_SUCCEEDED(list->HasMoreElements(&more)) && more) {
+    nsCOMPtr<nsISupports> next;
+    if (NS_FAILED(list->GetNext(getter_AddRefs(next)))) {
+      break;
+    }
+    nsCOMPtr<nsIFile> file = do_QueryInterface(next);
+    if (file) {
+      file->Normalize();
+      dirs.AppendElement(file);
+    }
+  }
 }
 
 struct CompareFilesByTime
 {
   bool
   LessThan(const nsCOMPtr<nsIFile>& a, const nsCOMPtr<nsIFile>& b) const
   {
     return GetPluginLastModifiedTime(a) < GetPluginLastModifiedTime(b);
@@ -1741,20 +1731,18 @@ nsresult nsPluginHost::ScanPluginsDirect
 
     if (nsPluginsDir::IsPluginFile(dirEntry)) {
       pluginFiles.AppendElement(dirEntry);
     }
   }
 
   pluginFiles.Sort(CompareFilesByTime());
 
-  nsCOMPtr<nsISimpleEnumerator> extensionDirs = GetExtensionDirectories();
-  if (!extensionDirs) {
-    PLUGIN_LOG(PLUGIN_LOG_ALWAYS, ("Could not get extension directories."));
-  }
+  nsCOMArray<nsIFile> extensionDirs;
+  GetExtensionDirectories(extensionDirs);
 
   bool warnOutdated = false;
 
   for (int32_t i = (pluginFiles.Length() - 1); i >= 0; i--) {
     nsCOMPtr<nsIFile>& localfile = pluginFiles[i];
 
     nsString utf16FilePath;
     rv = localfile->GetPath(utf16FilePath);
--- a/dom/plugins/test/testaddon/Makefile.in
+++ b/dom/plugins/test/testaddon/Makefile.in
@@ -1,23 +1,23 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include $(topsrcdir)/config/rules.mk
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),cocoa)
-plugin_file_name = Test.plugin
+plugin_file_names = Test.plugin SecondTest.plugin
 addon_file_name = testaddon_$(TARGET_XPCOM_ABI).xpi
 else
-plugin_file_name = $(DLL_PREFIX)nptest$(DLL_SUFFIX)
+plugin_file_names = $(DLL_PREFIX)nptest$(DLL_SUFFIX) $(DLL_PREFIX)npsecondtest$(DLL_SUFFIX)
 addon_file_name = testaddon.xpi
 endif
 
 # This is so hacky. Waiting on bug 988938.
 testdir = $(abspath $(DEPTH)/_tests/xpcshell/dom/plugins/test/unit/)
 addonpath = $(testdir)/$(addon_file_name)
 
 libs::
 	$(NSINSTALL) -D $(testdir)
 	rm -f $(addonpath)
 	cd $(srcdir) && zip -rD $(addonpath) install.rdf
-	cd $(DIST) && zip -rD $(addonpath) plugins/$(plugin_file_name)
+	cd $(DIST) && zip -rD $(addonpath) $(foreach name,$(plugin_file_names),plugins/$(name))
--- a/dom/plugins/test/unit/head_plugins.js
+++ b/dom/plugins/test/unit/head_plugins.js
@@ -9,37 +9,23 @@ Cu.import("resource://gre/modules/Promis
 
 const gIsWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
 const gIsOSX = ("nsILocalFileMac" in Ci);
 const gIsLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc) ||
   ("@mozilla.org/gio-service;1" in Cc);
 const gDirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
 
 // Finds the test plugin library
-function get_test_plugin() {
+function get_test_plugin(secondplugin=false) {
   var pluginEnum = gDirSvc.get("APluginsDL", Ci.nsISimpleEnumerator);
   while (pluginEnum.hasMoreElements()) {
     let dir = pluginEnum.getNext().QueryInterface(Ci.nsILocalFile);
+    let name = get_platform_specific_plugin_name(secondplugin);
     let plugin = dir.clone();
-    // OSX plugin
-    plugin.append("Test.plugin");
-    if (plugin.exists()) {
-      plugin.normalize();
-      return plugin;
-    }
-    plugin = dir.clone();
-    // *nix plugin
-    plugin.append("libnptest.so");
-    if (plugin.exists()) {
-      plugin.normalize();
-      return plugin;
-    }
-    // Windows plugin
-    plugin = dir.clone();
-    plugin.append("nptest.dll");
+    plugin.append(name);
     if (plugin.exists()) {
       plugin.normalize();
       return plugin;
     }
   }
   return null;
 }
 
@@ -88,21 +74,27 @@ function do_get_profile_startup() {
       throw Components.results.NS_ERROR_NO_INTERFACE;
     }
   };
   dirSvc.QueryInterface(Components.interfaces.nsIDirectoryService)
         .registerProvider(provider);
   return file.clone();
 }
 
-function get_platform_specific_plugin_name() {
-  if (gIsWindows) return "nptest.dll";
-  else if (gIsOSX) return "Test.plugin";
-  else if (gIsLinux) return "libnptest.so";
-  else return null;
+function get_platform_specific_plugin_name(secondplugin=false) {
+  if (secondplugin) {
+    if (gIsWindows) return "npsecondtest.dll";
+    if (gIsOSX) return "SecondTest.plugin";
+    if (gIsLinux) return "libnpsecondtest.so";
+  } else {
+    if (gIsWindows) return "nptest.dll";
+    if (gIsOSX) return "Test.plugin";
+    if (gIsLinux) return "libnptest.so";
+  }
+  return null;
 }
 
 function get_platform_specific_plugin_suffix() {
   if (gIsWindows) return ".dll";
   else if (gIsOSX) return ".plugin";
   else if (gIsLinux) return ".so";
   else return null;
 }
--- a/dom/plugins/test/unit/test_plugin_default_state_xpi.js
+++ b/dom/plugins/test/unit/test_plugin_default_state_xpi.js
@@ -36,16 +36,21 @@ function run_test() {
   run_next_test();
 }
 
 add_task(function* test_state() {
   // Remove test so we will have only one "Test Plug-in" registered.
   // xpcshell tests have plugins in per-test profiles, so that's fine.
   let file = get_test_plugin();
   file.remove(true);
+  file = get_test_plugin(true);
+  file.remove(true);
+
+  Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+  Services.prefs.setIntPref("plugin.defaultXpi.state", Ci.nsIPluginTag.STATE_ENABLED);
 
   let success = yield installAddon(getTestaddonFilename());
   Assert.ok(success, "Should have installed addon.");
   let addonDir = getAddonRoot(gProfileDir, ADDON_ID);
 
   let provider = {
     classID: Components.ID("{0af6b2d7-a06c-49b7-babc-636d292b0dbb}"),
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
@@ -100,9 +105,13 @@ add_task(function* test_state() {
   Assert.ok(!addon.userDisabled, "Addon should not be user disabled");
 
   let testPlugin = get_test_plugintag();
   Assert.notEqual(testPlugin, null, "Test plugin should have been found");
   Assert.equal(testPlugin.enabledState, Ci.nsIPluginTag.STATE_ENABLED, "Test plugin from addon should have state enabled");
 
   pluginDir.append(testPlugin.filename);
   Assert.ok(pluginDir.exists(), "Plugin file should exist in addon directory: " + pluginDir.path);
+
+  testPlugin = get_test_plugintag("Second Test Plug-in");
+  Assert.notEqual(testPlugin, null, "Second test plugin should have been found");
+  Assert.equal(testPlugin.enabledState, Ci.nsIPluginTag.STATE_ENABLED, "Second test plugin from addon should have state enabled");
 });
--- a/layout/tools/reftest/b2g_desktop.py
+++ b/layout/tools/reftest/b2g_desktop.py
@@ -59,17 +59,18 @@ class B2GDesktopReftest(RefTest):
                 if mozinfo.info['debug']:
                     options.timeout = 420
                 else:
                     options.timeout = 300
             self.timeout = options.timeout + 30.0
 
         log.info("%s | Running tests: start.", os.path.basename(__file__))
         cmd, args = self.build_command_line(options.app,
-                            ignore_window_size=options.ignoreWindowSize)
+                            ignore_window_size=options.ignoreWindowSize,
+                            browser_arg=options.browser_arg)
         self.runner = FirefoxRunner(profile=self.profile,
                                     binary=cmd,
                                     cmdargs=args,
                                     env=env,
                                     process_class=ProcessHandler,
                                     symbols_path=options.symbolsPath,
                                     kp_kwargs=kp_kwargs)
 
@@ -118,20 +119,24 @@ class B2GDesktopReftest(RefTest):
         # Set a future policy version to avoid the telemetry prompt.
         prefs["toolkit.telemetry.prompted"] = 999
         prefs["toolkit.telemetry.notifiedOptOut"] = 999
 
         # Set the extra prefs.
         profile.set_preferences(prefs)
         return profile
 
-    def build_command_line(self, app, ignore_window_size=False):
+    def build_command_line(self, app, ignore_window_size=False,
+                           browser_arg=None):
         cmd = os.path.abspath(app)
         args = ['-marionette']
 
+        if browser_arg:
+            args += [browser_arg]
+
         if not ignore_window_size:
             args.extend(['--screen', '800x1000'])
         return cmd, args
 
     def _on_output(self, line):
         print(line)
         # TODO use structured logging
         if "TEST-START" in line and "|" in line:
--- a/layout/tools/reftest/runreftestb2g.py
+++ b/layout/tools/reftest/runreftestb2g.py
@@ -26,16 +26,21 @@ class B2GOptions(ReftestOptions):
 
     def __init__(self, automation=None, **kwargs):
         defaults = {}
         if not automation:
             automation = B2GRemoteAutomation(None, "fennec", context_chrome=True)
 
         ReftestOptions.__init__(self, automation)
 
+        self.add_option("--browser-arg", action="store",
+                    type = "string", dest = "browser_arg",
+                    help = "Optional command-line arg to pass to the browser")
+        defaults["browser_arg"] = None
+
         self.add_option("--b2gpath", action="store",
                     type = "string", dest = "b2gPath",
                     help = "path to B2G repo or qemu dir")
         defaults["b2gPath"] = None
 
         self.add_option("--marionette", action="store",
                     type = "string", dest = "marionette",
                     help = "host:port to use when connecting to Marionette")
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1915,17 +1915,17 @@ abstract public class BrowserApp extends
     }
 
     /**
      * Add the provided item to the provided menu, which should be
      * the root (mMenu).
      */
     private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
         info.added = true;
-        
+
         final Menu destination;
         if (info.parent == 0) {
             destination = menu;
         } else if (info.parent == GECKO_TOOLS_MENU) {
             MenuItem tools = menu.findItem(R.id.tools);
             destination = tools != null ? tools.getSubMenu() : menu;
         } else {
             MenuItem parent = menu.findItem(info.parent);
@@ -2051,17 +2051,17 @@ abstract public class BrowserApp extends
         }
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         // Sets mMenu = menu.
         super.onCreateOptionsMenu(menu);
 
-        // Inform the menu about the action-items bar. 
+        // Inform the menu about the action-items bar.
         if (menu instanceof GeckoMenu &&
             HardwareUtils.isTablet()) {
             ((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar);
         }
 
         MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.browser_app_menu, mMenu);
 
@@ -2465,17 +2465,17 @@ abstract public class BrowserApp extends
             }
         }
         return super.onKeyLongPress(keyCode, event);
     }
 
     /*
      * If the app has been launched a certain number of times, and we haven't asked for feedback before,
      * open a new tab with about:feedback when launching the app from the icon shortcut.
-     */ 
+     */
     @Override
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
 
         String action = intent.getAction();
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 10 && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
             String uri = intent.getDataString();
@@ -2567,17 +2567,17 @@ abstract public class BrowserApp extends
             }
         }).execute();
     }
 
     // HomePager.OnNewTabsListener
     @Override
     public void onNewTabs(String[] urls) {
         final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB);
- 
+
         for (String url : urls) {
             if (!maybeSwitchToTab(url, flags)) {
                 openUrlAndStopEditing(url, true);
             }
         }
     }
 
     // HomePager.OnUrlOpenListener
--- a/mobile/android/base/gfx/TouchEventHandler.java
+++ b/mobile/android/base/gfx/TouchEventHandler.java
@@ -249,16 +249,18 @@ final class TouchEventHandler implements
 
         MotionEvent event = mEventQueue.poll();
         while (true) {
             // event being null here is valid and represents a block of events
             // that has already been dispatched.
 
             if (event != null) {
                 dispatchEvent(event, allowDefaultAction);
+                event.recycle();
+                event = null;
             }
             if (mEventQueue.isEmpty()) {
                 // we have processed the backlog of events, and are all caught up.
                 // now we can set clear the hold flag and set the dispatch flag so
                 // that the handleEvent() function can do the right thing for all
                 // remaining events in this block (which is still ongoing) without
                 // having to put them in the queue.
                 mHoldInQueue = false;
--- a/mobile/android/base/home/PanelItemView.java
+++ b/mobile/android/base/home/PanelItemView.java
@@ -73,16 +73,19 @@ class PanelItemView extends LinearLayout
                    .into(image);
         }
     }
 
     private static class ArticleItemView extends PanelItemView {
         private ArticleItemView(Context context) {
             super(context, R.layout.panel_article_item);
             setOrientation(LinearLayout.HORIZONTAL);
+
+            final int padding = getResources().getDimensionPixelSize(R.dimen.article_item_view_padding);
+            setPadding(0, padding, 0, padding);
         }
     }
 
     private static class ImageItemView extends PanelItemView {
         private ImageItemView(Context context) {
             super(context, R.layout.panel_image_item);
             setOrientation(LinearLayout.VERTICAL);
         }
--- a/mobile/android/base/home/TabMenuStrip.java
+++ b/mobile/android/base/home/TabMenuStrip.java
@@ -11,191 +11,90 @@ import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.HorizontalScrollView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-public class TabMenuStrip extends LinearLayout
-                          implements HomePager.Decor,
-                                     View.OnFocusChangeListener {
-    private static final String LOGTAG = "GeckoTabMenuStrip";
+/**
+ * {@code TabMenuStrip} is the view used to display {@code HomePager} tabs
+ * on tablets. See {@code TabMenuStripLayout} for details about how the
+ * tabs are created and updated.
+ */
+public class TabMenuStrip extends HorizontalScrollView
+                          implements HomePager.Decor {
 
-    private HomePager.OnTitleClickListener mOnTitleClickListener;
-    private Drawable mStrip;
-    private View mSelectedView;
+    // Offset between the selected tab title and the edge of the screen,
+    // except for the first and last tab in the tab strip.
+    private static final int TITLE_OFFSET_DIPS = 24;
 
-    // Data associated with the scrolling of the strip drawable.
-    private View toTab;
-    private View fromTab;
-    private float progress;
-
-    // This variable is used to predict the direction of scroll.
-    private float mPrevProgress;
+    private final int titleOffset;
+    private final TabMenuStripLayout layout;
 
     public TabMenuStrip(Context context, AttributeSet attrs) {
         super(context, attrs);
 
-        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip);
-        final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1);
-        a.recycle();
+        // Disable the scroll bar.
+        setHorizontalScrollBarEnabled(false);
 
-        if (stripResId != -1) {
-            mStrip = getResources().getDrawable(stripResId);
-        }
+        titleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);
 
-        setWillNotDraw(false);
+        layout = new TabMenuStripLayout(context, attrs);
+        addView(layout, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
     }
 
     @Override
     public void onAddPagerView(String title) {
-        final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
-        button.setText(title.toUpperCase());
-
-        addView(button);
-        button.setOnClickListener(new ViewClickListener(getChildCount() - 1));
-        button.setOnFocusChangeListener(this);
+        layout.onAddPagerView(title);
     }
 
     @Override
     public void removeAllPagerViews() {
-        removeAllViews();
+        layout.removeAllViews();
     }
 
     @Override
     public void onPageSelected(final int position) {
-        mSelectedView = getChildAt(position);
-
-        // Callback to measure and draw the strip after the view is visible.
-        ViewTreeObserver vto = mSelectedView.getViewTreeObserver();
-        if (vto.isAlive()) {
-            vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
-                @Override
-                public void onGlobalLayout() {
-                    mSelectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
-
-                    if (mStrip != null) {
-                        mStrip.setBounds(mSelectedView.getLeft(),
-                                         mSelectedView.getTop(),
-                                         mSelectedView.getRight(),
-                                         mSelectedView.getBottom());
-                    }
-
-                    mPrevProgress = position;
-                }
-            });
-        }
+        layout.onPageSelected(position);
     }
 
-    // Page scroll animates the drawable and its bounds from the previous to next child view.
     @Override
     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-        if (mStrip == null) {
-            return;
-        }
+        layout.onPageScrolled(position, positionOffset, positionOffsetPixels);
 
-        setScrollingData(position, positionOffset);
-
-        if (fromTab == null || toTab == null) {
+        final View selectedTitle = layout.getChildAt(position);
+        if (selectedTitle == null) {
             return;
         }
 
-        final int fromTabLeft =  fromTab.getLeft();
-        final int fromTabRight = fromTab.getRight();
-
-        final int toTabLeft =  toTab.getLeft();
-        final int toTabRight = toTab.getRight();
-
-        mStrip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)),
-                         0,
-                         (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
-                         getHeight());
-        invalidate();
-    }
+        final int selectedTitleOffset = (int) (positionOffset * selectedTitle.getWidth());
 
-    /*
-     * position + positionOffset goes from 0 to 2 as we scroll from page 1 to 3.
-     * Normalized progress is relative to the the direction the page is being scrolled towards.
-     * For this, we maintain direction of scroll with a state, and the child view we are moving towards and away from.
-     */
-    private void setScrollingData(int position, float positionOffset) {
-        if (position >= getChildCount() - 1) {
-            return;
-        }
-
-        final float currProgress = position + positionOffset;
-
-        if (mPrevProgress > currProgress) {
-            toTab = getChildAt(position);
-            fromTab = getChildAt(position + 1);
-            progress = 1 - positionOffset;
-        } else {
-            toTab = getChildAt(position + 1);
-            fromTab = getChildAt(position);
-            progress = positionOffset;
+        int titleLeft = selectedTitle.getLeft() + selectedTitleOffset;
+        if (position > 0) {
+            titleLeft -= titleOffset;
         }
 
-        mPrevProgress = currProgress;
-    }
-
-    @Override
-    public void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-
-        if (mStrip != null) {
-            mStrip.draw(canvas);
-        }
-    }
-
-    @Override
-    public void onFocusChange(View v, boolean hasFocus) {
-        if (v == this && hasFocus && getChildCount() > 0) {
-            mSelectedView.requestFocus();
-            return;
+        int titleRight = selectedTitle.getRight() + selectedTitleOffset;
+        if (position < layout.getChildCount() - 1) {
+            titleRight += titleOffset;
         }
 
-        if (!hasFocus) {
-            return;
-        }
-
-        int i = 0;
-        final int numTabs = getChildCount();
-
-        while (i < numTabs) {
-            View view = getChildAt(i);
-            if (view == v) {
-                view.requestFocus();
-                if (isShown()) {
-                    // A view is focused so send an event to announce the menu strip state.
-                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
-                }
-                break;
-            }
-
-            i++;
+        final int scrollX = getScrollX();
+        if (titleLeft < scrollX) {
+            // Tab strip overflows to the left.
+            scrollTo(titleLeft, 0);
+        } else if (titleRight > scrollX + getWidth()) {
+            // Tab strip overflows to the right.
+            scrollTo(titleRight - getWidth(), 0);
         }
     }
 
     @Override
     public void setOnTitleClickListener(HomePager.OnTitleClickListener onTitleClickListener) {
-        mOnTitleClickListener = onTitleClickListener;
-    }
-
-    private class ViewClickListener implements OnClickListener {
-        private final int mIndex;
-
-        public ViewClickListener(int index) {
-            mIndex = index;
-        }
-
-        @Override
-        public void onClick(View view) {
-            if (mOnTitleClickListener != null) {
-                mOnTitleClickListener.onTitleClicked(mIndex);
-            }
-        }
+        layout.setOnTitleClickListener(onTitleClickListener);
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/TabMenuStripLayout.java
@@ -0,0 +1,194 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * {@code TabMenuStripLayout} is the view that draws the {@code HomePager}
+ * tabs that are displayed in {@code TabMenuStrip}.
+ */
+class TabMenuStripLayout extends LinearLayout
+                         implements View.OnFocusChangeListener {
+
+    private HomePager.OnTitleClickListener onTitleClickListener;
+    private Drawable strip;
+    private View selectedView;
+
+    // Data associated with the scrolling of the strip drawable.
+    private View toTab;
+    private View fromTab;
+    private float progress;
+
+    // This variable is used to predict the direction of scroll.
+    private float prevProgress;
+
+    TabMenuStripLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabMenuStrip);
+        final int stripResId = a.getResourceId(R.styleable.TabMenuStrip_strip, -1);
+        a.recycle();
+
+        if (stripResId != -1) {
+            strip = getResources().getDrawable(stripResId);
+        }
+
+        setWillNotDraw(false);
+    }
+
+    void onAddPagerView(String title) {
+        final TextView button = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.tab_menu_strip, this, false);
+        button.setText(title.toUpperCase());
+
+        addView(button);
+        button.setOnClickListener(new ViewClickListener(getChildCount() - 1));
+        button.setOnFocusChangeListener(this);
+    }
+
+    void onPageSelected(final int position) {
+        selectedView = getChildAt(position);
+
+        // Callback to measure and draw the strip after the view is visible.
+        ViewTreeObserver vto = selectedView.getViewTreeObserver();
+        if (vto.isAlive()) {
+            vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+                @Override
+                public void onGlobalLayout() {
+                    selectedView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+
+                    if (strip != null) {
+                        strip.setBounds(selectedView.getLeft(),
+                                        selectedView.getTop(),
+                                        selectedView.getRight(),
+                                        selectedView.getBottom());
+                    }
+
+                    prevProgress = position;
+                }
+            });
+        }
+    }
+
+    // Page scroll animates the drawable and its bounds from the previous to next child view.
+    void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        if (strip == null) {
+            return;
+        }
+
+        setScrollingData(position, positionOffset);
+
+        if (fromTab == null || toTab == null) {
+            return;
+        }
+
+        final int fromTabLeft =  fromTab.getLeft();
+        final int fromTabRight = fromTab.getRight();
+
+        final int toTabLeft =  toTab.getLeft();
+        final int toTabRight = toTab.getRight();
+
+        strip.setBounds((int) (fromTabLeft + ((toTabLeft - fromTabLeft) * progress)),
+                         0,
+                         (int) (fromTabRight + ((toTabRight - fromTabRight) * progress)),
+                         getHeight());
+        invalidate();
+    }
+
+    /*
+     * position + positionOffset goes from 0 to 2 as we scroll from page 1 to 3.
+     * Normalized progress is relative to the the direction the page is being scrolled towards.
+     * For this, we maintain direction of scroll with a state, and the child view we are moving towards and away from.
+     */
+    void setScrollingData(int position, float positionOffset) {
+        if (position >= getChildCount() - 1) {
+            return;
+        }
+
+        final float currProgress = position + positionOffset;
+
+        if (prevProgress > currProgress) {
+            toTab = getChildAt(position);
+            fromTab = getChildAt(position + 1);
+            progress = 1 - positionOffset;
+        } else {
+            toTab = getChildAt(position + 1);
+            fromTab = getChildAt(position);
+            progress = positionOffset;
+        }
+
+        prevProgress = currProgress;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (strip != null) {
+            strip.draw(canvas);
+        }
+    }
+
+    @Override
+    public void onFocusChange(View v, boolean hasFocus) {
+        if (v == this && hasFocus && getChildCount() > 0) {
+            selectedView.requestFocus();
+            return;
+        }
+
+        if (!hasFocus) {
+            return;
+        }
+
+        int i = 0;
+        final int numTabs = getChildCount();
+
+        while (i < numTabs) {
+            View view = getChildAt(i);
+            if (view == v) {
+                view.requestFocus();
+                if (isShown()) {
+                    // A view is focused so send an event to announce the menu strip state.
+                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+                }
+                break;
+            }
+
+            i++;
+        }
+    }
+
+    void setOnTitleClickListener(HomePager.OnTitleClickListener onTitleClickListener) {
+        this.onTitleClickListener = onTitleClickListener;
+    }
+
+    private class ViewClickListener implements OnClickListener {
+        private final int mIndex;
+
+        public ViewClickListener(int index) {
+            mIndex = index;
+        }
+
+        @Override
+        public void onClick(View view) {
+            if (onTitleClickListener != null) {
+                onTitleClickListener.onTitleClicked(mIndex);
+            }
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -274,16 +274,17 @@ gbjar.sources += [
     'home/ReadingListPanel.java',
     'home/ReadingListRow.java',
     'home/SearchEngine.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',
     'home/SuggestClient.java',
     'home/TabMenuStrip.java',
+    'home/TabMenuStripLayout.java',
     'home/TopSitesGridItemView.java',
     'home/TopSitesGridView.java',
     'home/TopSitesPanel.java',
     'home/TopSitesThumbnailView.java',
     'home/TwoLinePageRow.java',
     'InputMethods.java',
     'JavaAddonManager.java',
     'LightweightTheme.java',
--- a/mobile/android/base/resources/layout/panel_article_item.xml
+++ b/mobile/android/base/resources/layout/panel_article_item.xml
@@ -3,29 +3,24 @@
    - 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <ImageView android:id="@+id/image"
                android:layout_width="54dp"
                android:layout_height="44dp"
-               android:layout_marginTop="10dip"
-               android:layout_marginBottom="10dip"
                android:layout_marginLeft="10dip"
                android:scaleType="centerCrop"/>
 
     <LinearLayout android:id="@+id/title_desc_container"
                   android:layout_width="fill_parent"
                   android:layout_height="wrap_content"
-                  android:paddingTop="15dip"
-                  android:paddingBottom="15dip"
                   android:paddingLeft="10dip"
                   android:paddingRight="10dip"
-                  android:minHeight="@dimen/page_row_height"
                   android:gravity="center_vertical"
                   android:orientation="vertical">
 
         <TextView android:id="@+id/title"
                   style="@style/Widget.PanelItemView.Title"
                   android:layout_width="fill_parent"
                   android:layout_height="wrap_content"/>
 
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -102,9 +102,12 @@
     <dimen name="home_banner_height">72dp</dimen>
 
     <!-- Icon Grid -->
     <dimen name="icongrid_columnwidth">128dp</dimen>
     <dimen name="icongrid_padding">16dp</dimen>
 
     <!-- PanelGridView dimensions -->
     <dimen name="panel_grid_view_column_width">150dp</dimen>
+
+    <!-- ArticleItemView dimensions -->
+    <dimen name="article_item_view_padding">15dp</dimen>
 </resources>
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -433,19 +433,18 @@ abstract class BaseTest extends BaseRobo
     public void selectMenuItemByPath(String[] listItems) {
         int listLength = listItems.length;
         if (listLength > 0) {
             selectMenuItem(listItems[0]);
         }
         if (listLength > 1) {
             for (int i = 1; i < listLength; i++) {
                 String itemName = "^" + listItems[i] + "$";
-                if (!waitForPreferencesText(itemName)) {
-                    mSolo.scrollDown();
-                }
+                mAsserter.ok(waitForPreferencesText(itemName), "Waiting for and scrolling once to find item " + itemName, itemName + " found");
+                mAsserter.ok(waitForEnabledText(itemName), "Waiting for enabled text " + itemName, itemName + " option is present and enabled");
                 mSolo.clickOnText(itemName);
             }
         }
     }
 
     public final void selectMenuItem(String menuItemName) {
         // build the item name ready to be used
         String itemName = "^" + menuItemName + "$";
--- a/mobile/android/base/tests/MotionEventHelper.java
+++ b/mobile/android/base/tests/MotionEventHelper.java
@@ -21,39 +21,54 @@ class MotionEventHelper {
         mSurfaceOffsetY = surfaceOffsetY;
         Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
     }
 
     public long down(float x, float y) {
         Log.d(LOGTAG, "Triggering down at (" + x + "," + y + ")");
         long downTime = SystemClock.uptimeMillis();
         MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
-        mInstrumentation.sendPointerSync(event);
+        try {
+            mInstrumentation.sendPointerSync(event);
+        } finally {
+            event.recycle();
+            event = null;
+        }
         return downTime;
     }
 
     public long move(long downTime, float x, float y) {
         return move(downTime, SystemClock.uptimeMillis(), x, y);
     }
 
     public long move(long downTime, long moveTime, float x, float y) {
         Log.d(LOGTAG, "Triggering move to (" + x + "," + y + ")");
         MotionEvent event = MotionEvent.obtain(downTime, moveTime, MotionEvent.ACTION_MOVE, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
-        mInstrumentation.sendPointerSync(event);
+        try {
+            mInstrumentation.sendPointerSync(event);
+        } finally {
+            event.recycle();
+            event = null;
+        }
         return downTime;
     }
 
     public long up(long downTime, float x, float y) {
         return up(downTime, SystemClock.uptimeMillis(), x, y);
     }
 
     public long up(long downTime, long upTime, float x, float y) {
         Log.d(LOGTAG, "Triggering up at (" + x + "," + y + ")");
         MotionEvent event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0);
-        mInstrumentation.sendPointerSync(event);
+        try {
+            mInstrumentation.sendPointerSync(event);
+        } finally {
+            event.recycle();
+            event = null;
+        }
         return -1L;
     }
 
     public Thread dragAsync(final float startX, final float startY, final float endX, final float endY, final long durationMillis) {
         Thread t = new Thread() {
             @Override
             public void run() {
                 int numEvents = (int)(durationMillis * DRAG_EVENTS_PER_SECOND / 1000);
--- a/mobile/android/base/tests/MotionEventReplayer.java
+++ b/mobile/android/base/tests/MotionEventReplayer.java
@@ -198,18 +198,23 @@ class MotionEventReplayer {
                             long.class, long.class, int.class, int.class, pointerIds.getClass(),
                             pointerData.getClass(), int.class, float.class, float.class,
                             int.class, int.class);
                     }
                     event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime,
                             eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData,
                             metaState, xPrecision, yPrecision, deviceId, edgeFlags);
                 }
-                Log.v(LOGTAG, "Injecting " + event.toString());
-                mInstrumentation.sendPointerSync(event);
+                try {
+                    Log.v(LOGTAG, "Injecting " + event.toString());
+                    mInstrumentation.sendPointerSync(event);
+                } finally {
+                    event.recycle();
+                    event = null;
+                }
 
                 eventProperties.clear();
             }
         } finally {
             br.close();
         }
     }
 }
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -45,16 +45,17 @@ import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MenuInflater;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupWindow;
@@ -185,17 +186,20 @@ public class BrowserToolbar extends Them
         final Resources res = getResources();
         urlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left);
         defaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
         urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
         urlBarEntry = findViewById(R.id.url_bar_entry);
         urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
 
         urlBarEntryDefaultLayoutParams = (RelativeLayout.LayoutParams) urlBarEntry.getLayoutParams();
-        urlBarEntryShrunkenLayoutParams = new RelativeLayout.LayoutParams(urlBarEntryDefaultLayoutParams);
+        // API level 19 adds a RelativeLayout.LayoutParams copy constructor, so we explicitly cast
+        // to ViewGroup.MarginLayoutParams to ensure consistency across platforms.
+        urlBarEntryShrunkenLayoutParams = new RelativeLayout.LayoutParams(
+                (ViewGroup.MarginLayoutParams) urlBarEntryDefaultLayoutParams);
         urlBarEntryShrunkenLayoutParams.addRule(RelativeLayout.ALIGN_RIGHT, R.id.edit_layout);
         urlBarEntryShrunkenLayoutParams.rightMargin = 0;
 
         // This will clip the translating edge's image at 60% of its width
         urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge);
         if (urlBarTranslatingEdge != null) {
             urlBarTranslatingEdge.getDrawable().setLevel(6000);
         }
@@ -392,17 +396,17 @@ public class BrowserToolbar extends Them
     }
 
     public void refresh() {
         urlDisplayLayout.dismissSiteIdentityPopup();
     }
 
     public boolean onBackPressed() {
         if (isEditing()) {
-            stopEditing();
+            cancelEdit();
             return true;
         }
 
         return urlDisplayLayout.dismissSiteIdentityPopup();
     }
 
     public boolean onKey(int keyCode, KeyEvent event) {
         if (event.getAction() != KeyEvent.ACTION_DOWN) {
--- a/testing/config/mozharness/b2g_desktop_config.py
+++ b/testing/config/mozharness/b2g_desktop_config.py
@@ -3,17 +3,18 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 config = {
     "mochitest_options": [
         "--console-level=INFO", "%(test_manifest)s",
         "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s",
         "--profile=%(gaia_profile)s", "--app=%(application)s", "--desktop",
         "--utility-path=%(utility_path)s", "--certificate-path=%(cert_path)s",
-        "--symbols-path=%(symbols_path)s",
+        "--symbols-path=%(symbols_path)s", "--browser-arg=%(browser_arg)s",
         "--quiet"
     ],
 
     "reftest_options": [
         "--desktop", "--profile=%(gaia_profile)s", "--appname=%(application)s",
-        "--symbols-path=%(symbols_path)s", "%(test_manifest)s",
+        "--browser-arg=%(browser_arg)s", "--symbols-path=%(symbols_path)s",
+        "%(test_manifest)s"
     ]
 }