author | Ryan VanderMeulen <ryanvm@gmail.com> |
Tue, 24 Mar 2015 11:58:07 -0400 | |
changeset 264227 | 47fa87252df0352a66c4698ceee71e351e8f2c8b |
parent 264226 | d026794b4c0b4a95414deaab831da79c27232586 (current diff) |
parent 264163 | 2e22440310321d890737f32af5ff31549ec70c6d (diff) |
child 264228 | ee8dc146ae8249d75410b3ede0650eb98564fc2c |
push id | 4718 |
push user | raliiev@mozilla.com |
push date | Mon, 11 May 2015 18:39:53 +0000 |
treeherder | mozilla-beta@c20c4ef55f08 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 39.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -10,17 +10,17 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
--- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -14,17 +14,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="527d1c939ee57deb7192166e56e2a3fffa8cb087"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -12,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="4efd19d199ae52656604f794c5a77518400220fd"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/> <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"/> @@ -130,12 +130,12 @@ <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/> <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/> <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/> <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/> <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="125ccf9bd5986c7728ea44508b3e1d1185ac028b"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c5f8d282efe4a4e8b1e31a37300944e338e60e4f"/> <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/> - <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/> + <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/> <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/> <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/> </manifest>
--- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -10,17 +10,17 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
--- a/b2g/config/emulator-l/sources.xml +++ b/b2g/config/emulator-l/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="52775e03a2d8532429dff579cb2cd56718e488c3"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>
--- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -14,17 +14,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="527d1c939ee57deb7192166e56e2a3fffa8cb087"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -10,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="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/> @@ -141,13 +141,13 @@ <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="2a1ded216a91bf62a72b1640cf01ab4998f37028"/> <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="a74adcf8d88320d936daa8d20ce88ca0107fb916"/> <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="9883ea57b0668d8f60dba025d4522dfa69a1fbfa"/> <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="a558dc844bf5144fc38603fd8f4df8d9557052a5"/> <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="57ee1320ed7b4a1a1274d8f3f6c177cd6b9becb2"/> <project name="platform/hardware/ril" path="hardware/ril" revision="12b1977cc704b35f2e9db2bb423fa405348bc2f3"/> <project name="platform/system/bluetooth" path="system/bluetooth" revision="985bf15264d865fe7b9c5b45f61c451cbaafa43d"/> <project name="platform/system/core" path="system/core" revision="42839aedcf70bf6bc92a3b7ea4a5cc9bf9aef3f9"/> - <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/> + <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/> <project name="platform/system/qcom" path="system/qcom" revision="63e3f6f176caad587d42bba4c16b66d953fb23c2"/> <project name="platform/vendor/qcom-opensource/wlan/prima" path="vendor/qcom/opensource/wlan/prima" revision="d8952a42771045fca73ec600e2b42a4c7129d723"/> <project name="platform/vendor/qcom/msm8610" path="device/qcom/msm8610" revision="4c187c1f3a0dffd8e51a961735474ea703535b99"/> </manifest>
--- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/> <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"/> @@ -140,13 +140,13 @@ <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="5e110615212302c5d798a3c223dcee458817651c"/> <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="fa9ffd47948eb24466de227e48fe9c4a7c5e7711"/> <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="cd76b19aafd4229ccf83853d02faef8c51ca8b34"/> <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="8a0d0b0d9889ef99c4c6317c810db4c09295f15a"/> <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="2208fa3537ace873b8f9ec2355055761c79dfd5f"/> <project name="platform/hardware/ril" path="hardware/ril" revision="c4e2ac95907a5519a0e09f01a0d8e27fec101af0"/> <project name="platform/system/bluetooth" path="system/bluetooth" revision="e1eb226fa3ad3874ea7b63c56a9dc7012d7ff3c2"/> <project name="platform/system/core" path="system/core" revision="adc485d8755af6a61641d197de7cfef667722580"/> - <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/> + <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/> <project name="platform/system/qcom" path="system/qcom" revision="1cdab258b15258b7f9657da70e6f06ebd5a2fc25"/> <project name="platform/vendor/qcom/msm8610" path="device/qcom/msm8610" revision="4ae5df252123591d5b941191790e7abed1bce5a4"/> <project name="platform/vendor/qcom-opensource/wlan/prima" path="vendor/qcom/opensource/wlan/prima" revision="ce18b47b4a4f93a581d672bbd5cb6d12fe796ca9"/> </manifest>
--- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { - "git_revision": "efebbafd12fc42ddcd378948b683a51106517660", + "git_revision": "00d50b78a7a99de5b0b55309a467807c677e2a66", "remote": "https://git.mozilla.org/releases/gaia.git", "branch": "" }, - "revision": "d8e53e5d917b1ce79aea842e8340ce82799cac3e", + "revision": "c1db04cf683e85135ca587f1f414d4456b20cfa0", "repo_path": "integration/gaia-central" }
--- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/> @@ -125,17 +125,17 @@ <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/> <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"/> <!-- Nexus 4 specific things --> <project name="device-mako" path="device/lge/mako" remote="b2g" revision="78d17f0c117f0c66dd55ee8d5c5dde8ccc93ecba"/> <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/> <project name="device/lge/mako-kernel" path="device/lge/mako-kernel" revision="d1729e53d71d711c8fde25eab8728ff2b9b4df0e"/> - <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/> + <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/> <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/> <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/> <project name="platform/hardware/broadcom/wlan" path="hardware/broadcom/wlan" revision="0e1929fa3aa38bf9d40e9e953d619fab8164c82e"/> <project name="platform/hardware/qcom/audio" path="hardware/qcom/audio" revision="b0a528d839cfd9d170d092fe3743b5252b4243a6"/> <project name="platform/hardware/qcom/bt" path="hardware/qcom/bt" revision="380945eaa249a2dbdde0daa4c8adb8ca325edba6"/> <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="6f3b0272cefaffeaed2a7d2bb8f633059f163ddc"/> <project name="platform/hardware/qcom/keymaster" path="hardware/qcom/keymaster" revision="16da8262c997a5a0d797885788a64a0771b26910"/> <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="689b476ba3eb46c34b81343295fe144a0e81a18e"/>
--- a/b2g/config/nexus-5-l/sources.xml +++ b/b2g/config/nexus-5-l/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="52775e03a2d8532429dff579cb2cd56718e488c3"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="efebbafd12fc42ddcd378948b683a51106517660"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="00d50b78a7a99de5b0b55309a467807c677e2a66"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/> <project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/> @@ -151,10 +151,10 @@ <project name="hardware_qcom_display" path="hardware/qcom/display" remote="b2g" revision="c43952000d57f08b93a0e4fb77052871ce587976"/> <project name="platform/hardware/qcom/keymaster" path="hardware/qcom/keymaster" revision="028649652cd8f8f18cfb47d34bd78c435eb030ca"/> <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="758a80fbb178b5663d4edbb46944b2dc553cb1ca"/> <project name="platform/hardware/qcom/msm8x74" path="hardware/qcom/msm8x74" revision="aa0124820e22302149b1f2db603a9a72f1972527"/> <project name="platform/hardware/qcom/power" path="hardware/qcom/power" revision="37499eb89f31233135ca73b830b067ab24dc1be2"/> <project name="platform/hardware/qcom/sensors" path="hardware/qcom/sensors" revision="3724fd91ef5183684d97e2bf1d7ff948faabe090"/> <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="2e54754cc0529d26ccac37ed291600048adbf6c0"/> <project name="platform/hardware/ril" path="hardware/ril" revision="71dfa8228ad0d6cdf6bac0426ac59404ab74b7f3"/> - <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="ab0e87a2be365b9c7470a9deca1eeaaa2f962b18"/> + <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="306b3290ea22211397d3daf49a42258f4638e4b7"/> </manifest>
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1293,17 +1293,17 @@ pref("services.sync.prefs.sync.dom.disab pref("services.sync.prefs.sync.dom.disable_window_flip", true); pref("services.sync.prefs.sync.dom.disable_window_move_resize", true); pref("services.sync.prefs.sync.dom.event.contextmenu.enabled", true); pref("services.sync.prefs.sync.extensions.personas.current", true); pref("services.sync.prefs.sync.extensions.update.enabled", true); pref("services.sync.prefs.sync.intl.accept_languages", true); pref("services.sync.prefs.sync.javascript.enabled", true); pref("services.sync.prefs.sync.layout.spellcheckDefault", true); -pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true); +pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true); pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true); pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true); pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true); pref("services.sync.prefs.sync.permissions.default.image", true); pref("services.sync.prefs.sync.pref.advanced.images.disable_button.view_image", true); pref("services.sync.prefs.sync.pref.advanced.javascript.disable_button.advanced", true); pref("services.sync.prefs.sync.pref.downloads.disable_button.edit_actions", true); pref("services.sync.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true); @@ -1879,8 +1879,9 @@ pref("dom.ipc.reportProcessHangs", true) pref("reader.parse-on-load.enabled", false); #endif // Enable ReadingList browser UI by default. pref("browser.readinglist.enabled", true); pref("browser.readinglist.sidebarEverOpened", false); // Enable the readinglist engine by default. pref("readinglist.scheduler.enabled", true); +pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
--- a/browser/base/content/browser-devedition.js +++ b/browser/base/content/browser-devedition.js @@ -4,17 +4,17 @@ /** * Listeners for the DevEdition theme. This adds an extra stylesheet * to browser.xul if a pref is set and no other themes are applied. */ let DevEdition = { _prefName: "browser.devedition.theme.enabled", _themePrefName: "general.skins.selectedSkin", - _lwThemePrefName: "lightweightThemes.isThemeSelected", + _lwThemePrefName: "lightweightThemes.selectedThemeID", _devtoolsThemePrefName: "devtools.theme", styleSheetLocation: "chrome://browser/skin/devedition.css", styleSheet: null, init: function () { this._updateDevtoolsThemeAttribute(); this._updateStyleSheetFromPrefs(); @@ -71,17 +71,17 @@ let DevEdition = { document.documentElement.setAttribute("devtoolstheme", devtoolsTheme); this._inferBrightness(); this._updateStyleSheetFromPrefs(); }, _updateStyleSheetFromPrefs: function() { let lightweightThemeSelected = false; try { - lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName); + lightweightThemeSelected = !!Services.prefs.getCharPref(this._lwThemePrefName); } catch(e) {} let defaultThemeSelected = false; try { defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0"; } catch(e) {} let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
--- a/browser/base/content/browser-syncui.js +++ b/browser/base/content/browser-syncui.js @@ -452,17 +452,17 @@ let gSyncUI = { // Return true if the reading-list is in a "prolonged" error state. That // engine doesn't impose what that means, so calculate it here. For // consistency, we just use the sync prefs. isProlongedReadingListError() { let lastSync, threshold, prolonged; try { lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync")); - threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout")); + threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000); prolonged = lastSync <= threshold; } catch (ex) { // no pref, assume not prolonged. prolonged = false; } this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}", {lastSync, threshold, prolonged}); return prolonged;
--- a/browser/base/content/test/general/browser_devedition.js +++ b/browser/base/content/test/general/browser_devedition.js @@ -1,44 +1,48 @@ /* * Testing changes for Developer Edition theme. * A special stylesheet should be added to the browser.xul document * when browser.devedition.theme.enabled is set to true and no themes * are applied. */ const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled"; -const PREF_LWTHEME = "lightweightThemes.isThemeSelected"; +const PREF_LWTHEME = "lightweightThemes.selectedThemeID"; +const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes"; const PREF_DEVTOOLS_THEME = "devtools.theme"; +const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {}); registerCleanupFunction(() => { // Set preferences back to their original values + LightweightThemeManager.currentTheme = null; Services.prefs.clearUserPref(PREF_DEVEDITION_THEME); Services.prefs.clearUserPref(PREF_LWTHEME); Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME); + Services.prefs.clearUserPref(PREF_LWTHEME_USED_THEMES); }); add_task(function* startTests() { Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark"); info ("Setting browser.devedition.theme.enabled to false."); Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false); ok (!DevEdition.styleSheet, "There is no devedition style sheet when the pref is false."); info ("Setting browser.devedition.theme.enabled to true."); Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true); ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set."); info ("Adding a lightweight theme."); - Services.prefs.setBoolPref(PREF_LWTHEME, true); + LightweightThemeManager.currentTheme = dummyLightweightTheme("preview0"); ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied."); info ("Removing a lightweight theme."); let onAttributeAdded = waitForBrightTitlebarAttribute(); - Services.prefs.setBoolPref(PREF_LWTHEME, false); + LightweightThemeManager.currentTheme = null; ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed."); yield onAttributeAdded; is (document.documentElement.getAttribute("brighttitlebarforeground"), "true", "The brighttitlebarforeground attribute is set on the window."); info ("Setting browser.devedition.theme.enabled to false."); Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false); @@ -80,26 +84,24 @@ add_task(function* testDevtoolsTheme() { ok (!document.documentElement.hasAttribute("brighttitlebarforeground"), "The brighttitlebarforeground attribute is not set on the window with light devtools theme."); }); function dummyLightweightTheme(id) { return { id: id, name: id, - headerURL: "http://lwttest.invalid/a.png", - footerURL: "http://lwttest.invalid/b.png", + headerURL: "resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg", + iconURL: "resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg", textcolor: "red", accentcolor: "blue" }; } add_task(function* testLightweightThemePreview() { - let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {}); - info ("Turning the pref on, then previewing lightweight themes"); Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true); ok (DevEdition.styleSheet, "The devedition stylesheet is enabled."); LightweightThemeManager.previewTheme(dummyLightweightTheme("preview0")); ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a lightweight theme preview."); LightweightThemeManager.resetPreview(); LightweightThemeManager.previewTheme(dummyLightweightTheme("preview1")); ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a second lightweight theme preview.");
--- a/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js +++ b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js @@ -44,17 +44,17 @@ add_task(function () { ok(installedThemeId.startsWith(firstLWThemeId), "The second theme in the 'My Themes' section should be the newly installed theme: " + "Installed theme id: " + installedThemeId + "; First theme ID: " + firstLWThemeId); is(header.nextSibling.nextSibling.nextSibling, recommendedHeader, "There should be two themes in the 'My Themes' section"); let defaultTheme = header.nextSibling; defaultTheme.doCommand(); - is(Services.prefs.getBoolPref("lightweightThemes.isThemeSelected"), false, "No lwtheme should be selected"); + is(Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"), false, "No lwtheme should be selected"); }); add_task(function asyncCleanup() { yield endCustomizing(); Services.prefs.clearUserPref("lightweightThemes.usedThemes"); Services.prefs.clearUserPref("lightweightThemes.recommendedThemes"); }) \ No newline at end of file
--- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -634,16 +634,23 @@ function injectLoopAPI(targetWindow) { TWO_WAY_MEDIA_CONN_LENGTH: { enumerable: true, get: function() { return Cu.cloneInto(TWO_WAY_MEDIA_CONN_LENGTH, targetWindow); } }, + SHARING_STATE_CHANGE: { + enumerable: true, + get: function() { + return Cu.cloneInto(SHARING_STATE_CHANGE, targetWindow); + } + }, + fxAEnabled: { enumerable: true, get: function() { return MozLoopService.fxAEnabled; }, }, logInToFxA: {
--- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -24,30 +24,44 @@ const LOOP_SESSION_TYPE = { */ const TWO_WAY_MEDIA_CONN_LENGTH = { SHORTER_THAN_10S: "SHORTER_THAN_10S", BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S", BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M", MORE_THAN_5M: "MORE_THAN_5M", }; +/** + * Buckets that we segment sharing state change telemetry probes into. + * + * @type {{WINDOW_ENABLED: String, WINDOW_DISABLED: String, + * BROWSER_ENABLED: String, BROWSER_DISABLED: String}} + */ +const SHARING_STATE_CHANGE = { + WINDOW_ENABLED: "WINDOW_ENABLED", + WINDOW_DISABLED: "WINDOW_DISABLED", + BROWSER_ENABLED: "BROWSER_ENABLED", + BROWSER_DISABLED: "BROWSER_DISABLED" +}; + // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error". const PREF_LOG_LEVEL = "loop.debug.loglevel"; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); Cu.importGlobalProperties(["URL"]); -this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", "TWO_WAY_MEDIA_CONN_LENGTH"]; +this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", + "TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE"]; XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport", "resource://gre/modules/media/RTCStatsReport.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
--- a/browser/components/loop/content/shared/js/actions.js +++ b/browser/components/loop/content/shared/js/actions.js @@ -356,17 +356,19 @@ loop.shared.actions = (function() { EmailRoomUrl: Action.define("emailRoomUrl", { roomUrl: String }), /** * XXX: should move to some roomActions module - refs bug 1079284 */ RoomFailure: Action.define("roomFailure", { - error: Object + error: Object, + // True when the failures occurs in the join room request to the loop-server. + failedJoinRequest: Boolean }), /** * Sets up the room information when it is received. * XXX: should move to some roomActions module - refs bug 1079284 * * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D */
--- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -100,17 +100,17 @@ loop.store.ActiveRoomStore = (function() actionData.error); this.setStoreState({ error: actionData.error, failureReason: getReason(actionData.error.errno) }); this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ? - ROOM_STATES.FULL : ROOM_STATES.FAILED); + ROOM_STATES.FULL : ROOM_STATES.FAILED, actionData.failedJoinRequest); }, /** * Registers the actions with the dispatcher that this store is interested * in after the initial setup has been performed. */ _registerPostSetupActions: function() { this.dispatcher.register(this, [ @@ -156,17 +156,20 @@ loop.store.ActiveRoomStore = (function() roomState: ROOM_STATES.GATHER, windowId: actionData.windowId }); // Get the window data from the mozLoop api. this._mozLoop.rooms.get(actionData.roomToken, function(error, roomData) { if (error) { - this.dispatchAction(new sharedActions.RoomFailure({error: error})); + this.dispatchAction(new sharedActions.RoomFailure({ + error: error, + failedJoinRequest: false + })); return; } this.dispatchAction(new sharedActions.SetupRoomInfo({ roomToken: actionData.roomToken, roomName: roomData.roomName, roomOwner: roomData.roomOwner, roomUrl: roomData.roomUrl @@ -288,17 +291,25 @@ loop.store.ActiveRoomStore = (function() * granted and starts joining the room. */ gotMediaPermission: function() { this.setStoreState({roomState: ROOM_STATES.JOINING}); this._mozLoop.rooms.join(this._storeState.roomToken, function(error, responseData) { if (error) { - this.dispatchAction(new sharedActions.RoomFailure({error: error})); + this.dispatchAction(new sharedActions.RoomFailure({ + error: error, + // This is an explicit flag to avoid the leave happening if join + // fails. We can't track it on ROOM_STATES.JOINING as the user + // might choose to leave the room whilst the XHR is in progress + // which would then mean we'd run the race condition of not + // notifying the server of a leave. + failedJoinRequest: true + })); return; } this.dispatchAction(new sharedActions.JoinedRoom({ apiKey: responseData.apiKey, sessionToken: responseData.sessionToken, sessionId: responseData.sessionId, expires: responseData.expires @@ -550,31 +561,37 @@ loop.store.ActiveRoomStore = (function() * Refreshes the membership of the room with the server, and then * sets up the refresh for the next cycle. */ _refreshMembership: function() { this._mozLoop.rooms.refreshMembership(this._storeState.roomToken, this._storeState.sessionToken, function(error, responseData) { if (error) { - this.dispatchAction(new sharedActions.RoomFailure({error: error})); + this.dispatchAction(new sharedActions.RoomFailure({ + error: error, + failedJoinRequest: false + })); return; } this._setRefreshTimeout(responseData.expires); }.bind(this)); }, /** * Handles leaving a room. Clears any membership timeouts, then * signals to the server the leave of the room. * - * @param {ROOM_STATES} nextState The next state to switch to. + * @param {ROOM_STATES} nextState The next state to switch to. + * @param {Boolean} failedJoinRequest Optional. Set to true if the join + * request to loop-server failed. It + * will skip the leave message. */ - _leaveRoom: function(nextState) { + _leaveRoom: function(nextState, failedJoinRequest) { if (loop.standaloneMedia) { loop.standaloneMedia.multiplexGum.reset(); } this._mozLoop.setScreenShareState( this.getStoreState().windowId, false); @@ -587,20 +604,21 @@ loop.store.ActiveRoomStore = (function() // We probably don't need to end screen share separately, but lets be safe. this._sdkDriver.disconnectSession(); if (this._timeout) { clearTimeout(this._timeout); delete this._timeout; } - if (this._storeState.roomState === ROOM_STATES.JOINING || - this._storeState.roomState === ROOM_STATES.JOINED || - this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED || - this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) { + if (!failedJoinRequest && + (this._storeState.roomState === ROOM_STATES.JOINING || + this._storeState.roomState === ROOM_STATES.JOINED || + this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED || + this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS)) { this._mozLoop.rooms.leave(this._storeState.roomToken, this._storeState.sessionToken); } this.setStoreState({roomState: nextState}); }, /**
--- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -160,16 +160,18 @@ loop.OTSdkDriver = (function() { } var config = _.extend(this._getCopyPublisherConfig(), options); this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(), config); this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this)); this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this)); + + this._noteSharingState(options.videoSource, true); }, /** * Initiates switching the browser window that is being shared. * * @param {Integer} windowId The windowId of the browser. */ switchAcquiredWindow: function(windowId) { @@ -191,16 +193,17 @@ loop.OTSdkDriver = (function() { if (!this.screenshare) { return false; } this.session.unpublish(this.screenshare); this.screenshare.off("accessAllowed accessDenied"); this.screenshare.destroy(); delete this.screenshare; + this._noteSharingState(this._windowId ? "browser" : "window", false); delete this._windowId; return true; }, /** * Connects a session for the SDK, listening to the required events. * * sessionData items: @@ -643,25 +646,25 @@ loop.OTSdkDriver = (function() { * Wrapper for adding a keyed value that also updates * connectionLengthNoted calls and sets the twoWayMediaStartTime to * this.CONNECTION_START_TIME_ALREADY_NOTED. * * @param {number} callLengthSeconds the call length in seconds * @private */ _noteConnectionLength: function(callLengthSeconds) { + var buckets = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH; - var bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S; - + var bucket = buckets.SHORTER_THAN_10S; if (callLengthSeconds >= 10 && callLengthSeconds <= 30) { - bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S; + bucket = buckets.BETWEEN_10S_AND_30S; } else if (callLengthSeconds > 30 && callLengthSeconds <= 300) { - bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M; + bucket = buckets.BETWEEN_30S_AND_5M; } else if (callLengthSeconds > 300) { - bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M; + bucket = buckets.MORE_THAN_5M; } this.mozLoop.telemetryAddKeyedValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH", bucket); this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_ALREADY_NOTED); this._connectionLengthNotedCalls++; if (this._debugTwoWayMediaTelemetry) { @@ -700,14 +703,39 @@ loop.OTSdkDriver = (function() { var callLengthSeconds = (endTime - startTime) / 1000; this._noteConnectionLength(callLengthSeconds); }, /** * If set to true, make it easy to test/verify 2-way media connection * telemetry code operation by viewing the logs. */ - _debugTwoWayMediaTelemetry: false + _debugTwoWayMediaTelemetry: false, + + /** + * Note the sharing state. If this.mozLoop is not defined, we're assumed to + * be running in the standalone client and return immediately. + * + * @param {String} type Type of sharing that was flipped. May be 'window' + * or 'tab'. + * @param {Boolean} enabled Flag that tells us if the feature was flipped on + * or off. + * @private + */ + _noteSharingState: function(type, enabled) { + if (!this.mozLoop) { + return; + } + + var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" + + (enabled ? "ENABLED" : "DISABLED")]; + if (!bucket) { + console.error("No sharing state bucket found for '" + type + "'"); + return; + } + + this.mozLoop.telemetryAddKeyedValue("LOOP_SHARING_STATE_CHANGE", bucket); + } }; return OTSdkDriver; })();
--- a/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js +++ b/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js @@ -42,8 +42,34 @@ add_task(function* test_mozLoop_telemetr } let snapshot = histogram.snapshot(); is(snapshot["SHORTER_THAN_10S"].sum, 1, "TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S"); is(snapshot["BETWEEN_10S_AND_30S"].sum, 2, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S"); is(snapshot["BETWEEN_30S_AND_5M"].sum, 3, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M"); is(snapshot["MORE_THAN_5M"].sum, 4, "TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M"); }); + +add_task(function* test_mozLoop_telemetryAdd_sharing_buckets() { + let histogramId = "LOOP_SHARING_STATE_CHANGE"; + let histogram = Services.telemetry.getKeyedHistogramById(histogramId); + const SHARING_STATES = gMozLoopAPI.SHARING_STATE_CHANGE; + + histogram.clear(); + for (let value of [SHARING_STATES.WINDOW_ENABLED, + SHARING_STATES.WINDOW_DISABLED, + SHARING_STATES.WINDOW_DISABLED, + SHARING_STATES.BROWSER_ENABLED, + SHARING_STATES.BROWSER_ENABLED, + SHARING_STATES.BROWSER_ENABLED, + SHARING_STATES.BROWSER_DISABLED, + SHARING_STATES.BROWSER_DISABLED, + SHARING_STATES.BROWSER_DISABLED, + SHARING_STATES.BROWSER_DISABLED]) { + gMozLoopAPI.telemetryAddKeyedValue(histogramId, value); + } + + let snapshot = histogram.snapshot(); + Assert.strictEqual(snapshot["WINDOW_ENABLED"].sum, 1, "SHARING_STATE_CHANGE.WINDOW_ENABLED"); + Assert.strictEqual(snapshot["WINDOW_DISABLED"].sum, 2, "SHARING_STATE_CHANGE.WINDOW_DISABLED"); + Assert.strictEqual(snapshot["BROWSER_ENABLED"].sum, 3, "SHARING_STATE_CHANGE.BROWSER_ENABLED"); + Assert.strictEqual(snapshot["BROWSER_DISABLED"].sum, 4, "SHARING_STATE_CHANGE.BROWSER_DISABLED"); +});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js +++ b/browser/components/loop/test/shared/activeRoomStore_test.js @@ -90,107 +90,149 @@ describe("loop.store.ActiveRoomStore", f store.setStoreState({ roomState: ROOM_STATES.JOINED, roomToken: "fakeToken", sessionToken: "1627384950" }); }); it("should log the error", function() { - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(console.error); sinon.assert.calledWith(console.error, sinon.match(ROOM_STATES.JOINED), fakeError); }); it("should set the state to `FULL` on server error room full", function() { fakeError.errno = REST_ERRNOS.ROOM_FULL; - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); expect(store._storeState.roomState).eql(ROOM_STATES.FULL); }); it("should set the state to `FAILED` on generic error", function() { - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN); }); it("should set the failureReason to EXPIRED_OR_INVALID on server error: " + "invalid token", function() { fakeError.errno = REST_ERRNOS.INVALID_TOKEN; - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID); }); it("should set the failureReason to EXPIRED_OR_INVALID on server error: " + "expired", function() { fakeError.errno = REST_ERRNOS.EXPIRED; - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID); }); it("should reset the multiplexGum", function() { - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(fakeMultiplexGum.reset); }); it("should set screen sharing inactive", function() { store.setStoreState({windowId: "1234"}); - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(fakeMozLoop.setScreenShareState); sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false); }); it("should disconnect from the servers via the sdk", function() { - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(fakeSdkDriver.disconnectSession); }); it("should clear any existing timeout", function() { sandbox.stub(window, "clearTimeout"); store._timeout = {}; - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(clearTimeout); }); it("should remove the sharing listener", function() { // Setup the listener. store.startScreenShare(new sharedActions.StartScreenShare({ type: "browser" })); // Now simulate room failure. - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(fakeMozLoop.removeBrowserSharingListener); }); it("should call mozLoop.rooms.leave", function() { - store.roomFailure({error: fakeError}); + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: false + })); sinon.assert.calledOnce(fakeMozLoop.rooms.leave); sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave, "fakeToken", "1627384950"); }); + + it("should not call mozLoop.rooms.leave if failedJoinRequest is true", function() { + store.roomFailure(new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: true + })); + + sinon.assert.notCalled(fakeMozLoop.rooms.leave); + }); }); describe("#setupWindowData", function() { var fakeToken, fakeRoomData; beforeEach(function() { fakeToken = "337-ff-54"; fakeRoomData = { @@ -266,17 +308,18 @@ describe("loop.store.ActiveRoomStore", f windowId: "42", type: "room", roomToken: fakeToken })); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.RoomFailure({ - error: fakeError + error: fakeError, + failedJoinRequest: false })); }); }); describe("#fetchServerData", function() { it("should save the token", function() { store.fetchServerData(new sharedActions.FetchServerData({ windowType: "room", @@ -444,17 +487,20 @@ describe("loop.store.ActiveRoomStore", f var fakeError = new Error("fake"); fakeMozLoop.rooms.join.callsArgWith(1, fakeError); store.gotMediaPermission(); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWith(dispatcher.dispatch, - new sharedActions.RoomFailure({error: fakeError})); + new sharedActions.RoomFailure({ + error: fakeError, + failedJoinRequest: true + })); }); }); describe("#joinedRoom", function() { var fakeJoinedData; beforeEach(function() { fakeJoinedData = { @@ -577,17 +623,18 @@ describe("loop.store.ActiveRoomStore", f // Clock tick for the first expiry time (which // sets up the refreshMembership). sandbox.clock.tick(fakeJoinedData.expires * 1000); sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.RoomFailure({ - error: fakeError + error: fakeError, + failedJoinRequest: false })); }); }); describe("#connectedToSdkServers", function() { it("should set the state to `SESSION_CONNECTED`", function() { store.connectedToSdkServers(new sharedActions.ConnectedToSdkServers());
--- a/browser/components/loop/test/shared/otSdkDriver_test.js +++ b/browser/components/loop/test/shared/otSdkDriver_test.js @@ -66,16 +66,22 @@ describe("loop.OTSdkDriver", function () mozLoop = { telemetryAddKeyedValue: sinon.stub(), TWO_WAY_MEDIA_CONN_LENGTH: { SHORTER_THAN_10S: "SHORTER_THAN_10S", BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S", BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M", MORE_THAN_5M: "MORE_THAN_5M" + }, + SHARING_STATE_CHANGE: { + WINDOW_ENABLED: "WINDOW_ENABLED", + WINDOW_DISABLED: "WINDOW_DISABLED", + BROWSER_ENABLED: "BROWSER_ENABLED", + BROWSER_DISABLED: "BROWSER_DISABLED" } }; driver = new loop.OTSdkDriver({ dispatcher: dispatcher, sdk: sdk, mozLoop: mozLoop, isDesktop: true @@ -184,16 +190,17 @@ describe("loop.OTSdkDriver", function () }); }); describe("#startScreenShare", function() { var fakeElement; beforeEach(function() { sandbox.stub(dispatcher, "dispatch"); + sandbox.stub(driver, "_noteSharingState"); fakeElement = { className: "fakeVideo" }; driver.getScreenShareElementFunc = function() { return fakeElement; }; @@ -209,16 +216,29 @@ describe("loop.OTSdkDriver", function () scrollWithPage: true } }; driver.startScreenShare(options); sinon.assert.calledOnce(sdk.initPublisher); sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options); }); + + it("should log a telemetry action", function() { + var options = { + videoSource: "browser", + constraints: { + browserWindow: 42, + scrollWithPage: true + } + }; + driver.startScreenShare(options); + + sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true); + }); }); describe("#switchAcquiredWindow", function() { beforeEach(function() { var options = { videoSource: "browser", constraints: { browserWindow: 42, @@ -246,36 +266,80 @@ describe("loop.OTSdkDriver", function () sinon.assert.notCalled(publisher._.switchAcquiredWindow); }); }); describe("#endScreenShare", function() { beforeEach(function() { driver.getScreenShareElementFunc = function() {}; + sandbox.stub(dispatcher, "dispatch"); + sandbox.stub(driver, "_noteSharingState"); + }); + + it("should unpublish the share", function() { driver.startScreenShare({ videoSource: "window" }); - - sandbox.stub(dispatcher, "dispatch"); + driver.session = session; - driver.session = session; - }); - - it("should unpublish the share", function() { driver.endScreenShare(new sharedActions.EndScreenShare()); sinon.assert.calledOnce(session.unpublish); }); + it("should log a telemetry action", function() { + driver.startScreenShare({ + videoSource: "window" + }); + driver.session = session; + + driver.endScreenShare(new sharedActions.EndScreenShare()); + + sinon.assert.calledWithExactly(driver._noteSharingState, "window", false); + }); + it("should destroy the share", function() { + driver.startScreenShare({ + videoSource: "window" + }); + driver.session = session; + expect(driver.endScreenShare()).to.equal(true); sinon.assert.calledOnce(publisher.destroy); }); + + it("should unpublish the share too when type is 'browser'", function() { + driver.startScreenShare({ + videoSource: "browser", + constraints: { + browserWindow: 42 + } + }); + driver.session = session; + + driver.endScreenShare(new sharedActions.EndScreenShare()); + + sinon.assert.calledOnce(session.unpublish); + }); + + it("should log a telemetry action too when type is 'browser'", function() { + driver.startScreenShare({ + videoSource: "browser", + constraints: { + browserWindow: 42 + } + }); + driver.session = session; + + driver.endScreenShare(new sharedActions.EndScreenShare()); + + sinon.assert.calledWithExactly(driver._noteSharingState, "browser", false); + }); }); describe("#connectSession", function() { it("should initialise a new session", function() { driver.connectSession(sessionData); sinon.assert.calledOnce(sdk.initSession); sinon.assert.calledWithExactly(sdk.initSession, "3216549870"); @@ -426,16 +490,54 @@ describe("loop.OTSdkDriver", function () driver._isDesktop = false; driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS); sinon.assert.notCalled(mozLoop.telemetryAddKeyedValue); }); }); + describe("#_noteSharingState", function() { + it("should record enabled sharing states for window", function() { + driver._noteSharingState("window", true); + + sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue); + sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue, + "LOOP_SHARING_STATE_CHANGE", + mozLoop.SHARING_STATE_CHANGE.WINDOW_ENABLED); + }); + + it("should record enabled sharing states for browser", function() { + driver._noteSharingState("browser", true); + + sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue); + sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue, + "LOOP_SHARING_STATE_CHANGE", + mozLoop.SHARING_STATE_CHANGE.BROWSER_ENABLED); + }); + + it("should record disabled sharing states for window", function() { + driver._noteSharingState("window", false); + + sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue); + sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue, + "LOOP_SHARING_STATE_CHANGE", + mozLoop.SHARING_STATE_CHANGE.WINDOW_DISABLED); + }); + + it("should record disabled sharing states for browser", function() { + driver._noteSharingState("browser", false); + + sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue); + sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue, + "LOOP_SHARING_STATE_CHANGE", + mozLoop.SHARING_STATE_CHANGE.BROWSER_DISABLED); + }); + }); + describe("#forceDisconnectAll", function() { it("should not disconnect anything when not connected", function() { driver.session = session; driver.forceDisconnectAll(function() {}); sinon.assert.notCalled(session.forceDisconnect); });
--- a/browser/components/readinglist/ReadingList.jsm +++ b/browser/components/readinglist/ReadingList.jsm @@ -53,16 +53,27 @@ const ITEM_RECORD_PROPERTIES = ` addedBy addedOn storedOn markedReadBy markedReadOn readPosition `.trim().split(/\s+/); +// Article objects that are passed to ReadingList.addItem may contain +// some properties that are known but are not currently stored in the +// ReadingList records. This is the list of properties that are knowingly +// disregarded before the item is normalized. +const ITEM_DISREGARDED_PROPERTIES = ` + byline + dir + content + length +`.trim().split(/\s+/); + /** * A reading list contains ReadingListItems. * * A list maintains only one copy of an item per URL. So if for example you use * an iterator to get two references to items with the same URL, your references * actually refer to the same JS object. * * Options Objects @@ -848,16 +859,19 @@ ReadingListItemIterator.prototype = { * aren't in ITEM_RECORD_PROPERTIES. * * @param record A non-normalized record object. * @return The new normalized record. */ function normalizeRecord(nonNormalizedRecord) { let record = {}; for (let prop in nonNormalizedRecord) { + if (ITEM_DISREGARDED_PROPERTIES.includes(prop)) { + continue; + } if (!ITEM_RECORD_PROPERTIES.includes(prop)) { throw new Error("Unrecognized item property: " + prop); } switch (prop) { case "url": case "resolvedURL": if (nonNormalizedRecord[prop]) { record[prop] = normalizeURI(nonNormalizedRecord[prop]).spec;
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/ServerClient.jsm @@ -0,0 +1,166 @@ +/* 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/. */ + +// The client used to access the ReadingList server. + +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js"); +XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js"); +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm"); + +let log = Log.repository.getLogger("readinglist.serverclient"); + +const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request. + +this.EXPORTED_SYMBOLS = [ + "ServerClient", +]; + +// utf-8 joy. rest.js, which we use for the underlying requests, does *not* +// encode the request as utf-8 even though it wants to know the encoding. +// It does, however, explicitly decode the response. This seems insane, but is +// what it is. +// The end result being we need to utf-8 the request and let the response take +// care of itself. +function objectToUTF8Json(obj) { + // FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;) + return CommonUtils.encodeUTF8(JSON.stringify(obj)); +} + +function ServerClient(fxa = fxAccounts) { + this.fxa = fxa; +} + +ServerClient.prototype = { + + request(options) { + return this._request(options.path, options.method, options.body, options.headers); + }, + + get serverURL() { + return Services.prefs.getCharPref("readinglist.server"); + }, + + _getURL(path) { + let result = this.serverURL; + // we expect the path to have a leading slash, so remove any trailing + // slashes on the pref. + if (result.endsWith("/")) { + result = result.slice(0, -1); + } + return result + path; + }, + + // Hook points for testing. + _getToken() { + // Assume token-caching is in place - if it's not we should avoid doing + // this each request. + return this.fxa.getOAuthToken({scope: OAUTH_SCOPE}); + }, + + _removeToken(token) { + // XXX - remove this check once tokencaching landsin FxA. + if (!this.fxa.removeCachedOAuthToken) { + dump("XXX - token caching support is yet to land - can't remove token!"); + return; + } + return this.fxa.removeCachedOAuthToken({token}); + }, + + // Converts an error from the RESTRequest object to an error we export. + _convertRestError(error) { + return error; // XXX - errors? + }, + + // Converts an error from a try/catch handler to an error we export. + _convertJSError(error) { + return error; // XXX - errors? + }, + + /* + * Perform a request - handles authentication + */ + _request: Task.async(function* (path, method, body, headers) { + let token = yield this._getToken(); + let response = yield this._rawRequest(path, method, body, headers, token); + log.debug("initial request got status ${status}", response); + if (response.status == 401) { + // an auth error - assume our token has expired or similar. + this._removeToken(token); + token = yield this._getToken(); + response = yield this._rawRequest(path, method, body, headers, token); + log.debug("retry of request got status ${status}", response); + } + return response; + }), + + /* + * Perform a request *without* abstractions such as auth etc + * + * On success (which *includes* non-200 responses) returns an object like: + * { + * status: 200, # http status code + * headers: {}, # header values keyed by header name. + * body: {}, # parsed json + } + */ + + _rawRequest(path, method, body, headers, oauthToken) { + return new Promise((resolve, reject) => { + let url = this._getURL(path); + log.debug("dispatching request to", url); + let request = new RESTRequest(url); + method = method.toUpperCase(); + + request.setHeader("Accept", "application/json"); + request.setHeader("Content-Type", "application/json; charset=utf-8"); + request.setHeader("Authorization", "Bearer " + oauthToken); + // and additional header specified for this request. + if (headers) { + for (let [headerName, headerValue] in Iterator(headers)) { + log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue}); + request.setHeader(headerName, headerValue); + } + } + + request.onComplete = error => { + if (error) { + return reject(this._convertRestError(error)); + } + + let response = request.response; + log.debug("received response status: ${status} ${statusText}", response); + // Handle response status codes we know about + let result = { + status: response.status, + headers: response.headers + }; + try { + if (response.body) { + result.body = JSON.parse(response.body); + } + } catch (e) { + log.info("Failed to parse JSON body |${body}|: ${e}", + {body: response.body, e}); + // We don't reject due to this (and don't even make a huge amount of + // log noise - eg, a 50X error from a load balancer etc may not write + // JSON. + } + + resolve(result); + } + // We are assuming the body has already been decoded and thus contains + // unicode, but the server expects utf-8. encodeURIComponent does that. + request.dispatch(method, objectToUTF8Json(body)); + }); + }, +};
--- a/browser/components/readinglist/moz.build +++ b/browser/components/readinglist/moz.build @@ -1,25 +1,23 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. JAR_MANIFESTS += ['jar.mn'] EXTRA_JS_MODULES.readinglist += [ 'ReadingList.jsm', + 'Scheduler.jsm', + 'ServerClient.jsm', 'SQLiteStore.jsm', ] TESTING_JS_MODULES += [ 'test/ReadingListTestUtils.jsm', ] BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini'] XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini'] -EXTRA_JS_MODULES.readinglist += [ - 'Scheduler.jsm', -] - with Files('**'): BUG_COMPONENT = ('Firefox', 'Reading List')
--- a/browser/components/readinglist/sidebar.js +++ b/browser/components/readinglist/sidebar.js @@ -156,16 +156,17 @@ let RLSidebar = { itemNode.querySelector(".item-domain").textContent = domain; let thumb = itemNode.querySelector(".item-thumb-container"); if (item.preview) { thumb.style.backgroundImage = "url(" + item.preview + ")"; } else { thumb.style.removeProperty("background-image"); } + thumb.classList.toggle("preview-available", !!item.preview); }, /** * Ensure that the list is populated with the correct items. */ ensureListItems: Task.async(function* () { yield ReadingList.forEachItem(item => { // TODO: Should be batch inserting via DocumentFragment @@ -195,17 +196,17 @@ let RLSidebar = { }, set activeItem(node) { if (node && node.parentNode != this.list) { log.error(`Unable to set activeItem to invalid node ${node}`); return; } - log.debug(`Setting activeItem: ${node ? node.id : null}`); + log.trace(`Setting activeItem: ${node ? node.id : null}`); if (node && node.classList.contains("active")) { return; } let prevItem = document.querySelector("#list > .item.active"); if (prevItem) { prevItem.classList.remove("active"); @@ -228,17 +229,17 @@ let RLSidebar = { }, set selectedItem(node) { if (node && node.parentNode != this.list) { log.error(`Unable to set selectedItem to invalid node ${node}`); return; } - log.debug(`Setting activeItem: ${node ? node.id : null}`); + log.trace(`Setting selectedItem: ${node ? node.id : null}`); let prevItem = document.querySelector("#list > .item.selected"); if (prevItem) { prevItem.classList.remove("selected"); } if (node) { node.classList.add("selected"); @@ -265,17 +266,17 @@ let RLSidebar = { if (item.classList.contains("selected")) { return i; } } return -1; }, set selectedIndex(index) { - log.debug(`Setting selectedIndex: ${index}`); + log.trace(`Setting selectedIndex: ${index}`); if (index == -1) { this.selectedItem = null; return; } let item = this.list.children.item(index); if (!item) {
--- a/browser/components/readinglist/test/xpcshell/head.js +++ b/browser/components/readinglist/test/xpcshell/head.js @@ -1,7 +1,56 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); + +do_get_profile(); // fxa needs a profile directory for storage. + +Cu.import("resource://gre/modules/FxAccounts.jsm"); +Cu.import("resource://gre/modules/FxAccountsClient.jsm"); + +// Create a mocked FxAccounts object with a signed-in, verified user. +function* createMockFxA() { + + function MockFxAccountsClient() { + this._email = "nobody@example.com"; + this._verified = false; + + this.accountStatus = function(uid) { + let deferred = Promise.defer(); + deferred.resolve(!!uid && (!this._deletedOnServer)); + return deferred.promise; + }; + + this.signOut = function() { return Promise.resolve(); }; + + FxAccountsClient.apply(this); + } + + MockFxAccountsClient.prototype = { + __proto__: FxAccountsClient.prototype + } + + function MockFxAccounts() { + return new FxAccounts({ + fxAccountsClient: new MockFxAccountsClient(), + getAssertion: () => Promise.resolve("assertion"), + }); + } + + let fxa = new MockFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + assertion: "foobar", + sessionToken: "dead", + kA: "beef", + kB: "cafe", + verified: true + }; + + yield fxa.setSignedInUser(credentials); + return fxa; +}
new file mode 100644 --- /dev/null +++ b/browser/components/readinglist/test/xpcshell/test_ServerClient.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource:///modules/readinglist/ServerClient.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); + +let appender = new Log.DumpAppender(); +for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) { + Log.repository.getLogger(logName).addAppender(appender); +} + +// Some test servers we use. +let Server = function(handlers) { + this._server = null; + this._handlers = handlers; +} + +Server.prototype = { + start() { + this._server = new HttpServer(); + for (let [path, handler] in Iterator(this._handlers)) { + // httpd.js seems to swallow exceptions + let thisHandler = handler; + let wrapper = (request, response) => { + try { + thisHandler(request, response); + } catch (ex) { + print("**** Handler for", path, "failed:", ex, ex.stack); + throw ex; + } + } + this._server.registerPathHandler(path, wrapper); + } + this._server.start(-1); + }, + + stop() { + return new Promise(resolve => { + this._server.stop(resolve); + this._server = null; + }); + }, + + get host() { + return "http://localhost:" + this._server.identity.primaryPort; + }, +}; + +// An OAuth server that hands out tokens. +function OAuthTokenServer() { + let server; + let handlers = { + "/v1/authorization": (request, response) => { + response.setStatusLine("1.1", 200, "OK"); + let token = "token" + server.numTokenFetches; + print("Test OAuth server handing out token", token); + server.numTokenFetches += 1; + server.activeTokens.add(token); + response.write(JSON.stringify({access_token: token})); + }, + "/v1/destroy": (request, response) => { + // Getting the body seems harder than it should be! + let sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(request.bodyInputStream); + let body = JSON.parse(sis.read(sis.available())); + sis.close(); + let token = body.token; + ok(server.activeTokens.delete(token)); + print("after destroy have", server.activeTokens.size, "tokens left.") + response.setStatusLine("1.1", 200, "OK"); + response.write('{}'); + }, + } + server = new Server(handlers); + server.numTokenFetches = 0; + server.activeTokens = new Set(); + return server; +} + +// The tests. +function run_test() { + run_next_test(); +} + +// Arrange for the first token we hand out to be rejected - the client should +// notice the 401 and silently get a new token and retry the request. +add_task(function testAuthRetry() { + let handlers = { + "/v1/batch": (request, response) => { + // We know the first token we will get is "token0", so we simulate that + // "expiring" by only accepting "token1". Then we just echo the response + // back. + let authHeader; + try { + authHeader = request.getHeader("Authorization"); + } catch (ex) {} + if (authHeader != "Bearer token1") { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.write("wrong token"); + return; + } + response.setStatusLine("1.1", 200, "OK"); + response.write(JSON.stringify({ok: true})); + } + }; + let rlserver = new Server(handlers); + rlserver.start(); + let authServer = OAuthTokenServer(); + authServer.start(); + try { + Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1"); + Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1"); + + let fxa = yield createMockFxA(); + let sc = new ServerClient(fxa); + + let response = yield sc.request({ + path: "/batch", + method: "post", + body: {foo: "bar"}, + }); + equal(response.status, 200, "got the 200 we expected"); + equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200") + deepEqual(response.body, {ok: true}); + } finally { + yield authServer.stop(); + yield rlserver.stop(); + } +}); + +// Check that specified headers are seen by the server, and that server headers +// in the response are seen by the client. +add_task(function testHeaders() { + let handlers = { + "/v1/batch": (request, response) => { + ok(request.hasHeader("x-foo"), "got our foo header"); + equal(request.getHeader("x-foo"), "bar", "foo header has the correct value"); + response.setHeader("Server-Sent-Header", "hello"); + response.setStatusLine("1.1", 200, "OK"); + response.write("{}"); + } + }; + let rlserver = new Server(handlers); + rlserver.start(); + try { + Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1"); + + let fxa = yield createMockFxA(); + let sc = new ServerClient(fxa); + sc._getToken = () => Promise.resolve(); + + let response = yield sc.request({ + path: "/batch", + method: "post", + headers: {"X-Foo": "bar"}, + body: {foo: "bar"}}); + equal(response.status, 200, "got the 200 we expected"); + equal(response.headers["server-sent-header"], "hello", "got the server header"); + } finally { + yield rlserver.stop(); + } +}); + +// Check that unicode ends up as utf-8 in requests, and vice-versa in responses. +// (Note the ServerClient assumes all strings in and out are UCS, and thus have +// already been encoded/decoded (ie, it never expects to receive stuff already +// utf-8 encoded, and never returns utf-8 encoded responses.) +add_task(function testUTF8() { + let handlers = { + "/v1/hello": (request, response) => { + // Get the body as bytes. + let sis = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + sis.init(request.bodyInputStream); + let body = sis.read(sis.available()); + sis.close(); + // The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol. + // It should have been encoded as utf-8 which is \xc2\xa9 + equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data"); + // and just write it back unchanged. + response.setStatusLine("1.1", 200, "OK"); + response.write(body); + } + }; + let rlserver = new Server(handlers); + rlserver.start(); + try { + Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1"); + + let fxa = yield createMockFxA(); + let sc = new ServerClient(fxa); + sc._getToken = () => Promise.resolve(); + + let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol + let response = yield sc.request({ + path: "/hello", + method: "post", + body: body + }); + equal(response.status, 200, "got the 200 we expected"); + deepEqual(response.body, body); + } finally { + yield rlserver.stop(); + } +});
--- a/browser/components/readinglist/test/xpcshell/xpcshell.ini +++ b/browser/components/readinglist/test/xpcshell/xpcshell.ini @@ -1,7 +1,8 @@ [DEFAULT] head = head.js firefox-appdir = browser [test_ReadingList.js] +[test_ServerClient.js] [test_scheduler.js] [test_SQLiteStore.js]
--- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -40,17 +40,17 @@ const WINDOW_ATTRIBUTES = ["width", "hei // Hideable window features to (re)store // Restored in restoreWindowFeatures() const WINDOW_HIDEABLE_FEATURES = [ "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars" ]; // Messages that will be received via the Frame Message Manager. -const FMM_MESSAGES = [ +const MESSAGES = [ // The content script gives us a reference to an object that performs // synchronous collection of session data. "SessionStore:setupSyncHandler", // The content script sends us data that has been invalidated and needs to // be saved to disk. "SessionStore:update", @@ -65,37 +65,39 @@ const FMM_MESSAGES = [ // consider restoring another tab in the queue. The document has // been restored, and forms have been filled. We trigger // SSTabRestored at this time. "SessionStore:restoreTabContentComplete", // A tab that is being restored was reloaded. We call restoreTabContent to // finish restoring it right away. "SessionStore:reloadPendingTab", + + // A crashed tab was revived by navigating to a different page. Remove its + // browser from the list of crashed browsers to stop ignoring its messages. + "SessionStore:crashedTabRevived", ]; // The list of messages we accept from <xul:browser>s that have no tab // assigned. Those are for example the ones that preload about:newtab pages. -const FMM_NOTAB_MESSAGES = new Set([ +const NOTAB_MESSAGES = new Set([ // For a description see above. "SessionStore:setupSyncHandler", // For a description see above. "SessionStore:update", ]); -// Messages that will be received via the Parent Process Message Manager. -const PPMM_MESSAGES = [ - // A tab is being revived from the crashed state. The sender of this - // message should actually be running in the parent process, since this - // will be the crashed tab interface. We use the Child and Parent Process - // Message Managers because the message is sent during framescript unload - // when the Frame Message Manager is not available. - "SessionStore:RemoteTabRevived", -]; +// The list of messages we want to receive even during the short period after a +// frame has been removed from the DOM and before its frame script has finished +// unloading. +const CLOSED_MESSAGES = new Set([ + // For a description see above. + "SessionStore:crashedTabRevived", +]); // These are tab events that we listen to. const TAB_EVENTS = [ "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned", "TabUnpinned" ]; // The number of milliseconds in a day @@ -418,18 +420,16 @@ let SessionStoreInternal = { throw new Error("SessionStore.init() must only be called once!"); } TelemetryTimestamps.add("sessionRestoreInitialized"); OBSERVING.forEach(function(aTopic) { Services.obs.addObserver(this, aTopic, true); }, this); - PPMM_MESSAGES.forEach(msg => ppmm.addMessageListener(msg, this)); - this._initPrefs(); this._initialized = true; }, /** * Initialize the session using the state provided by SessionStartup */ initSession: function () { @@ -549,18 +549,16 @@ let SessionStoreInternal = { SessionSaver.run(); } // clear out priority queue in case it's still holding refs TabRestoreQueue.reset(); // Make sure to cancel pending saves. SessionSaver.cancel(); - - PPMM_MESSAGES.forEach(msg => ppmm.removeMessageListener(msg, this)); }, /** * Handle notifications */ observe: function ssi_observe(aSubject, aTopic, aData) { switch (aTopic) { case "browser-window-before-show": // catch new windows @@ -597,31 +595,25 @@ let SessionStoreInternal = { }, /** * This method handles incoming messages sent by the session store content * script via the Frame Message Manager or Parent Process Message Manager, * and thus enables communication with OOP tabs. */ receiveMessage(aMessage) { - // We'll deal with any Parent Process Message Manager messages first... - if (aMessage.name == "SessionStore:RemoteTabRevived") { - this._crashedBrowsers.delete(aMessage.objects.browser.permanentKey); - return; - } - // If we got here, that means we're dealing with a frame message // manager message, so the target will be a <xul:browser>. var browser = aMessage.target; var win = browser.ownerDocument.defaultView; let tab = win.gBrowser.getTabForBrowser(browser); // Ensure we receive only specific messages from <xul:browser>s that // have no tab assigned, e.g. the ones that preload about:newtab pages. - if (!tab && !FMM_NOTAB_MESSAGES.has(aMessage.name)) { + if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) { throw new Error(`received unexpected message '${aMessage.name}' ` + `from a browser that has no tab`); } switch (aMessage.name) { case "SessionStore:setupSyncHandler": TabState.setSyncHandler(browser, aMessage.objects.handler); break; @@ -704,16 +696,19 @@ let SessionStoreInternal = { break; case "SessionStore:reloadPendingTab": if (this.isCurrentEpoch(browser, aMessage.data.epoch)) { if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { this.restoreTabContent(tab); } } break; + case "SessionStore:crashedTabRevived": + this._crashedBrowsers.delete(browser.permanentKey); + break; default: throw new Error(`received unknown message '${aMessage.name}'`); break; } }, /** * Record telemetry measurements stored in an object. @@ -794,17 +789,20 @@ let SessionStoreInternal = { if (RunState.isQuitting) return; // Assign the window a unique identifier we can use to reference // internal data about the window. aWindow.__SSi = this._generateWindowID(); let mm = aWindow.getGroupMessageManager("browsers"); - FMM_MESSAGES.forEach(msg => mm.addMessageListener(msg, this)); + MESSAGES.forEach(msg => { + let listenWhenClosed = CLOSED_MESSAGES.has(msg); + mm.addMessageListener(msg, this, listenWhenClosed); + }); // Load the frame script after registering listeners. mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); // and create its data object this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false }; let isPrivateWindow = false; @@ -1123,17 +1121,17 @@ let SessionStoreInternal = { for (let i = 0; i < tabbrowser.tabs.length; i++) { this.onTabRemove(aWindow, tabbrowser.tabs[i], true); } // Cache the window state until it is completely gone. DyingWindowCache.set(aWindow, winData); let mm = aWindow.getGroupMessageManager("browsers"); - FMM_MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); + MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); delete aWindow.__SSi; }, /** * On quit application requested */ onQuitApplicationRequested: function ssi_onQuitApplicationRequested() {
--- a/browser/components/sessionstore/content/content-sessionStore.js +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -744,22 +744,18 @@ function handleRevivedTab() { if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { // Sanity check - we'd better be loading this in a non-remote browser. throw new Error("We seem to be navigating away from about:tabcrashed in " + "a non-remote browser. This should really never happen."); } removeEventListener("pagehide", handleRevivedTab); - // We can't send a message using the frame message manager because by - // the time we reach the unload event handler, it's "too late", and messages - // won't be sent or received. The child-process message manager works though, - // despite the fact that we're really running in the parent process. - let browser = docShell.chromeEventHandler; - cpmm.sendAsyncMessage("SessionStore:RemoteTabRevived", null, {browser: browser}); + // Notify the parent. + sendAsyncMessage("SessionStore:crashedTabRevived"); } } // If we're browsing from the tab crashed UI to a blacklisted URI that keeps // this browser non-remote, we'll handle that in a pagehide event. addEventListener("pagehide", handleRevivedTab); addEventListener("unload", () => {
--- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -26,32 +26,33 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm"); const EventEmitter = devtools.require("devtools/toolkit/event-emitter"); const Telemetry = devtools.require("devtools/shared/telemetry"); const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR"; const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR"; -const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL"; -const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_EXPONENTIAL"; +const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR"; +const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR"; const FORBIDDEN_IDS = new Set(["toolbox", ""]); const MAX_ORDINAL = 99; const bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties"); /** * DevTools is a class that represents a set of developer tools, it holds a * set of tools and keeps track of open toolboxes in the browser. */ this.DevTools = function DevTools() { this._tools = new Map(); // Map<toolId, tool> this._themes = new Map(); // Map<themeId, theme> this._toolboxes = new Map(); // Map<target, toolbox> + this._telemetry = new Telemetry(); // destroy() is an observer's handler so we need to preserve context. this.destroy = this.destroy.bind(this); this._teardown = this._teardown.bind(this); this._testing = false; EventEmitter.decorate(this);
--- a/browser/devtools/shared/devices.js +++ b/browser/devtools/shared/devices.js @@ -1,603 +1,74 @@ /* 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 { Ci, Cc } = require("chrome"); +const { getJSON } = require("devtools/shared/getjson"); const { Services } = require("resource://gre/modules/Services.jsm"); +const promise = require("promise"); + +const DEVICES_URL = "devtools.devices.url"; const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/device.properties"); -/* `Devices` is a catalog of existing devices and their properties, intended - * for (mobile) device emulation tools and features. +/* This is a catalog of common web-enabled devices and their properties, + * intended for (mobile) device emulation. * * The properties of a device are: - * - name: Device brand and model(s). - * - width: Viewport width. - * - height: Viewport height. - * - pixelRatio: Screen pixel ratio to viewport. - * - userAgent: Device UserAgent string. - * - touch: Whether the screen is touch-enabled. + * - name: brand and model(s). + * - width: viewport width. + * - height: viewport height. + * - pixelRatio: ratio from viewport to physical screen pixels. + * - userAgent: UA string of the device's browser. + * - touch: whether it has a touch screen. + * - firefoxOS: whether Firefox OS is supported. * - * To add more devices to this catalog, either patch this file, or push new - * device descriptions from your own code (e.g. an addon) like so: + * The device types are: + * ["phones", "tablets", "laptops", "televisions", "consoles", "watches"]. + * + * You can easily add more devices to this catalog from your own code (e.g. an + * addon) like so: * * var myPhone = { name: "My Phone", ... }; - * require("devtools/shared/devices").Devices.Others.phones.push(myPhone); + * require("devtools/shared/devices").AddDevice(myPhone, "phones"); */ -let Devices = { - Types: ["phones", "tablets", "notebooks", "televisions", "watches"], - - // Get the localized string of a device type. - GetString(deviceType) { - return Strings.GetStringFromName("device." + deviceType); - }, -}; -exports.Devices = Devices; - +// Local devices catalog that addons can add to. +let localDevices = {}; -// The `Devices.FirefoxOS` list was put together from various sources online. -Devices.FirefoxOS = { - phones: [ - { - name: "Firefox OS Flame", - width: 320, - height: 570, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Alcatel One Touch Fire, Fire C", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Alcatel Fire E", - width: 320, - height: 480, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Geeksphone Keon", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Geeksphone Peak, Revolution", - width: 360, - height: 640, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Intex Cloud Fx", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "LG Fireweb", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; LG-D300; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Spice Fire One Mi-FX1", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Symphony GoFox F15", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "Zen Fire 105", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "ZTE Open", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; ZTEOPEN; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "ZTE Open C", - width: 320, - height: 450, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Mobile; OPENC; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - ], - tablets: [ - { - name: "Foxconn InFocus", - width: 1280, - height: 800, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - { - name: "VIA Vixen", - width: 1024, - height: 600, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0", - touch: true, - }, - ], - notebooks: [ - ], - televisions: [ - { - name: "720p HD Television", - width: 1280, - height: 720, - pixelRatio: 1, - userAgent: "", - touch: false, - }, - { - name: "1080p Full HD Television", - width: 1920, - height: 1080, - pixelRatio: 1, - userAgent: "", - touch: false, - }, - { - name: "4K Ultra HD Television", - width: 3840, - height: 2160, - pixelRatio: 1, - userAgent: "", - touch: false, - }, - ], - watches: [ - { - name: "LG G Watch", - width: 280, - height: 280, - pixelRatio: 1, - userAgent: "", - touch: true, - }, - { - name: "LG G Watch R", - width: 320, - height: 320, - pixelRatio: 1, - userAgent: "", - touch: true, - }, - { - name: "Moto 360", - width: 320, - height: 290, - pixelRatio: 1, - userAgent: "", - touch: true, - }, - { - name: "Samsung Gear Live", - width: 320, - height: 320, - pixelRatio: 1, - userAgent: "", - touch: true, - }, - ], -}; +// Add a device to the local catalog. +function AddDevice(device, type = "phones") { + let list = localDevices[type]; + if (!list) { + list = localDevices[type] = []; + } + list.push(device); +} +exports.AddDevice = AddDevice; + +// Get the complete devices catalog. +function GetDevices(bypassCache = false) { + let deferred = promise.defer(); -// `Devices.Others` was derived from the Chromium source code: -// - chromium/src/third_party/WebKit/Source/devtools/front_end/toolbox/OverridesUI.js -Devices.Others = { - phones: [ - { - name: "Apple iPhone 3GS", - width: 320, - height: 480, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5", - touch: true, - }, - { - name: "Apple iPhone 4", - width: 320, - height: 480, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5", - touch: true, - }, - { - name: "Apple iPhone 5", - width: 320, - height: 568, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", - touch: true, - }, - { - name: "Apple iPhone 6", - width: 375, - height: 667, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4", - touch: true, - }, - { - name: "Apple iPhone 6 Plus", - width: 414, - height: 736, - pixelRatio: 3, - userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4", - touch: true, - }, - { - name: "BlackBerry Z10", - width: 384, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+", - touch: true, - }, - { - name: "BlackBerry Z30", - width: 360, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+", - touch: true, - }, - { - name: "Google Nexus 4", - width: 384, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", - touch: true, - }, - { - name: "Google Nexus 5", - width: 360, - height: 640, - pixelRatio: 3, - userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", - touch: true, - }, - { - name: "Google Nexus S", - width: 320, - height: 533, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "HTC Evo, Touch HD, Desire HD, Desire", - width: 320, - height: 533, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "HTC One X, EVO LTE", - width: 360, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; Android 4.0.3; HTC One X Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19", - touch: true, - }, - { - name: "HTC Sensation, Evo 3D", - width: 360, - height: 640, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - touch: true, - }, - { - name: "LG Optimus 2X, Optimus 3D, Optimus Black", - width: 320, - height: 533, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; LG-P990/V08c Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MMS/LG-Android-MMS-V1.0/1.2", - touch: true, - }, - { - name: "LG Optimus G", - width: 384, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; Android 4.0; LG-E975 Build/IMM76L) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", - touch: true, - }, - { - name: "LG Optimus LTE, Optimus 4X HD", - width: 424, - height: 753, - pixelRatio: 1.7, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; LG-P930 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "LG Optimus One", - width: 213, - height: 320, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; LG-MS690 Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "Motorola Defy, Droid, Droid X, Milestone", - width: 320, - height: 569, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17", - touch: true, - }, - { - name: "Motorola Droid 3, Droid 4, Droid Razr, Atrix 4G, Atrix 2", - width: 540, - height: 960, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "Motorola Droid Razr HD", - width: 720, - height: 1280, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; DROID RAZR 4G Build/6.5.1-73_DHD-11_M1-29) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "Nokia C5, C6, C7, N97, N8, X7", - width: 360, - height: 640, - pixelRatio: 1, - userAgent: "NokiaN97/21.1.107 (SymbianOS/9.4; Series60/5.0 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebkit/525 (KHTML, like Gecko) BrowserNG/7.1.4", - touch: true, - }, - { - name: "Nokia Lumia 7X0, Lumia 8XX, Lumia 900, N800, N810, N900", - width: 320, - height: 533, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 820)", - touch: true, - }, - { - name: "Samsung Galaxy Note 3", - width: 360, - height: 640, - pixelRatio: 3, - userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - touch: true, - }, - { - name: "Samsung Galaxy Note II", - width: 360, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - touch: true, - }, - { - name: "Samsung Galaxy Note", - width: 400, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SAMSUNG-SGH-I717 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "Samsung Galaxy S III, Galaxy Nexus", - width: 360, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - touch: true, - }, - { - name: "Samsung Galaxy S, S II, W", - width: 320, - height: 533, - pixelRatio: 1.5, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.1; en-us; GT-I9000 Build/ECLAIR) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", - touch: true, - }, - { - name: "Samsung Galaxy S4", - width: 360, - height: 640, - pixelRatio: 3, - userAgent: "Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36", - touch: true, - }, - { - name: "Sony Xperia S, Ion", - width: 360, - height: 640, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; LT28at Build/6.1.C.1.111) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - touch: true, - }, - { - name: "Sony Xperia Sola, U", - width: 480, - height: 854, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SonyEricssonST25i Build/6.0.B.1.564) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "Sony Xperia Z, Z1", - width: 360, - height: 640, - pixelRatio: 3, - userAgent: "Mozilla/5.0 (Linux; U; Android 4.2; en-us; SonyC6903 Build/14.1.G.1.518) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", - touch: true, - }, - ], - tablets: [ - { - name: "Amazon Kindle Fire HDX 7″", - width: 1920, - height: 1200, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; U; en-us; KFTHWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", - touch: true, - }, - { - name: "Amazon Kindle Fire HDX 8.9″", - width: 2560, - height: 1600, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true", - touch: true, - }, - { - name: "Amazon Kindle Fire (First Generation)", - width: 1024, - height: 600, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.141.16-Gen4_11004310) AppleWebkit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true", - touch: true, - }, - { - name: "Apple iPad 1 / 2 / iPad Mini", - width: 1024, - height: 768, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5", - touch: true, - }, - { - name: "Apple iPad 3 / 4", - width: 1024, - height: 768, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", - touch: true, - }, - { - name: "BlackBerry PlayBook", - width: 1024, - height: 600, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+", - touch: true, - }, - { - name: "Google Nexus 10", - width: 1280, - height: 800, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36", - touch: true, - }, - { - name: "Google Nexus 7 2", - width: 960, - height: 600, - pixelRatio: 2, - userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36", - touch: true, - }, - { - name: "Google Nexus 7", - width: 966, - height: 604, - pixelRatio: 1.325, - userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36", - touch: true, - }, - { - name: "Motorola Xoom, Xyboard", - width: 1280, - height: 800, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2", - touch: true, - }, - { - name: "Samsung Galaxy Tab 7.7, 8.9, 10.1", - width: 1280, - height: 800, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - { - name: "Samsung Galaxy Tab", - width: 1024, - height: 600, - pixelRatio: 1, - userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - touch: true, - }, - ], - notebooks: [ - { - name: "Notebook with touch", - width: 1280, - height: 950, - pixelRatio: 1, - userAgent: "", - touch: true, - }, - { - name: "Notebook with HiDPI screen", - width: 1440, - height: 900, - pixelRatio: 2, - userAgent: "", - touch: false, - }, - { - name: "Generic notebook", - width: 1280, - height: 800, - pixelRatio: 1, - userAgent: "", - touch: false, - }, - ], - televisions: [ - ], - watches: [ - ], -}; + // Fetch common devices from Mozilla's CDN. + getJSON(DEVICES_URL, bypassCache).then(devices => { + for (let type in localDevices) { + if (!devices[type]) { + devices.TYPES.push(type); + devices[type] = []; + } + devices[type] = localDevices[type].concat(devices[type]); + } + deferred.resolve(devices); + }); + + return deferred.promise; +} +exports.GetDevices = GetDevices; + +// Get the localized string for a device type. +function GetDeviceString(deviceType) { + return Strings.GetStringFromName("device." + deviceType); +} +exports.GetDeviceString = GetDeviceString;
rename from browser/devtools/webide/modules/remote-resources.js rename to browser/devtools/shared/getjson.js --- a/browser/devtools/webide/modules/remote-resources.js +++ b/browser/devtools/shared/getjson.js @@ -1,54 +1,43 @@ /* 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/. */ const {Cu, CC} = require("chrome"); -const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +const promise = require("promise"); const {Services} = Cu.import("resource://gre/modules/Services.jsm"); const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1"); -function getJSON(bypassCache, pref) { +// Downloads and caches a JSON file from a URL given by the pref. +exports.getJSON = function (prefName, bypassCache) { if (!bypassCache) { try { - let str = Services.prefs.getCharPref(pref + "_cache"); + let str = Services.prefs.getCharPref(prefName + "_cache"); let json = JSON.parse(str); return promise.resolve(json); } catch(e) {/* no pref or invalid json. Let's continue */} } - let deferred = promise.defer(); - let xhr = new XMLHttpRequest(); xhr.onload = () => { let json; try { json = JSON.parse(xhr.responseText); } catch(e) { - return deferred.reject("Not valid JSON"); + return deferred.reject("Invalid JSON"); } - Services.prefs.setCharPref(pref + "_cache", xhr.responseText); + Services.prefs.setCharPref(prefName + "_cache", xhr.responseText); deferred.resolve(json); } xhr.onerror = (e) => { deferred.reject("Network error"); } - xhr.open("get", Services.prefs.getCharPref(pref)); + xhr.open("get", Services.prefs.getCharPref(prefName)); xhr.send(); return deferred.promise; } - - - -exports.GetTemplatesJSON = function(bypassCache) { - return getJSON(bypassCache, "devtools.webide.templatesURL"); -} - -exports.GetAddonsJSON = function(bypassCache) { - return getJSON(bypassCache, "devtools.webide.addonsURL"); -}
--- a/browser/devtools/shared/moz.build +++ b/browser/devtools/shared/moz.build @@ -46,25 +46,27 @@ EXTRA_JS_MODULES.devtools.shared.timelin ] EXTRA_JS_MODULES.devtools.shared += [ 'autocomplete-popup.js', 'd3.js', 'devices.js', 'doorhanger.js', 'frame-script-utils.js', + 'getjson.js', 'inplace-editor.js', 'observable-object.js', 'options-view.js', 'telemetry.js', 'theme-switching.js', 'theme.js', 'undo.js', ] EXTRA_JS_MODULES.devtools.shared.widgets += [ + 'widgets/CubicBezierPresets.js', 'widgets/CubicBezierWidget.js', 'widgets/FastListWidget.js', 'widgets/Spectrum.js', 'widgets/TableWidget.js', 'widgets/Tooltip.js', 'widgets/TreeWidget.js', ]
--- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -2,24 +2,28 @@ subsuite = devtools support-files = browser_layoutHelpers.html browser_layoutHelpers-getBoxQuads.html browser_layoutHelpers_iframe.html browser_templater_basic.html browser_toolbar_basic.html browser_toolbar_webconsole_errors_count.html + browser_devices.json doc_options-view.xul head.js leakhunt.js [browser_css_color.js] [browser_cubic-bezier-01.js] [browser_cubic-bezier-02.js] [browser_cubic-bezier-03.js] +[browser_cubic-bezier-04.js] +[browser_cubic-bezier-05.js] +[browser_cubic-bezier-06.js] [browser_flame-graph-01.js] [browser_flame-graph-02.js] [browser_flame-graph-03a.js] [browser_flame-graph-03b.js] [browser_flame-graph-03c.js] [browser_flame-graph-04.js] [browser_flame-graph-utils-01.js] [browser_flame-graph-utils-02.js] @@ -93,8 +97,9 @@ skip-if = e10s # Bug 1086492 - Disable t [browser_templater_basic.js] [browser_toolbar_basic.js] [browser_toolbar_tooltip.js] [browser_toolbar_webconsole_errors_count.js] skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't correct with e10s [browser_treeWidget_basic.js] [browser_treeWidget_keyboard_interaction.js] [browser_treeWidget_mouse_interaction.js] +[browser_devices.js]
--- a/browser/devtools/shared/test/browser_cubic-bezier-01.js +++ b/browser/devtools/shared/test/browser_cubic-bezier-01.js @@ -2,26 +2,30 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Tests that the CubicBezierWidget generates content in a given parent node const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; -const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {CubicBezierWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); add_task(function*() { yield promiseTab("about:blank"); let [host, win, doc] = yield createHost("bottom", TEST_URI); - info("Checking that the markup is created in the parent"); + info("Checking that the graph markup is created in the parent"); let container = doc.querySelector("#container"); let w = new CubicBezierWidget(container); + ok(container.querySelector(".display-wrap"), + "The display has been added"); + ok(container.querySelector(".coordinate-plane"), "The coordinate plane has been added"); let buttons = container.querySelectorAll("button"); is(buttons.length, 2, "The 2 control points have been added"); is(buttons[0].className, "control-point"); is(buttons[0].id, "P1"); is(buttons[1].className, "control-point");
--- a/browser/devtools/shared/test/browser_cubic-bezier-02.js +++ b/browser/devtools/shared/test/browser_cubic-bezier-02.js @@ -2,145 +2,192 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Tests the CubicBezierWidget events const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; -const {CubicBezierWidget, PREDEFINED} = +const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets"); add_task(function*() { yield promiseTab("about:blank"); let [host, win, doc] = yield createHost("bottom", TEST_URI); + // Required or widget will be clipped inside of 'bottom' + // host by -14. Setting `fixed` zeroes this which is needed for + // calculating offsets. Occurs in test env only. + doc.body.setAttribute("style", "position: fixed"); + let container = doc.querySelector("#container"); let w = new CubicBezierWidget(container, PREDEFINED.linear); - yield pointsCanBeDragged(w, win, doc); - yield curveCanBeClicked(w, win, doc); - yield pointsCanBeMovedWithKeyboard(w, win, doc); + let rect = w.curve.getBoundingClientRect(); + rect.graphTop = rect.height * w.bezierCanvas.padding[0]; + rect.graphBottom = rect.height - rect.graphTop; + rect.graphHeight = rect.graphBottom - rect.graphTop; + + yield pointsCanBeDragged(w, win, doc, rect); + yield curveCanBeClicked(w, win, doc, rect); + yield pointsCanBeMovedWithKeyboard(w, win, doc, rect); w.destroy(); host.destroy(); gBrowser.removeCurrentTab(); }); -function* pointsCanBeDragged(widget, win, doc) { +function* pointsCanBeDragged(widget, win, doc, offsets) { info("Checking that the control points can be dragged with the mouse"); info("Listening for the update event"); let onUpdated = widget.once("updated"); info("Generating a mousedown/move/up on P1"); widget._onPointMouseDown({target: widget.p1}); - doc.onmousemove({pageX: 0, pageY: 100}); + doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop}); doc.onmouseup(); let bezier = yield onUpdated; ok(true, "The widget fired the updated event"); ok(bezier, "The updated event contains a bezier argument"); is(bezier.P1[0], 0, "The new P1 time coordinate is correct"); is(bezier.P1[1], 1, "The new P1 progress coordinate is correct"); info("Listening for the update event"); onUpdated = widget.once("updated"); info("Generating a mousedown/move/up on P2"); widget._onPointMouseDown({target: widget.p2}); - doc.onmousemove({pageX: 200, pageY: 300}); + doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom}); doc.onmouseup(); bezier = yield onUpdated; is(bezier.P2[0], 1, "The new P2 time coordinate is correct"); is(bezier.P2[1], 0, "The new P2 progress coordinate is correct"); } -function* curveCanBeClicked(widget, win, doc) { +function* curveCanBeClicked(widget, win, doc, offsets) { info("Checking that clicking on the curve moves the closest control point"); info("Listening for the update event"); let onUpdated = widget.once("updated"); info("Click close to P1"); - widget._onCurveClick({pageX: 50, pageY: 150}); + let x = offsets.left + (offsets.width / 4.0); + let y = offsets.graphTop + (offsets.graphHeight / 4.0); + widget._onCurveClick({pageX: x, pageY: y}); let bezier = yield onUpdated; ok(true, "The widget fired the updated event"); is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); is(bezier.P2[0], 1, "P2 time coordinate remained unchanged"); is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged"); info("Listening for the update event"); onUpdated = widget.once("updated"); info("Click close to P2"); - widget._onCurveClick({pageX: 150, pageY: 250}); + x = offsets.right - (offsets.width / 4); + y = offsets.graphBottom - (offsets.graphHeight / 4); + widget._onCurveClick({pageX: x, pageY: y}); bezier = yield onUpdated; is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct"); is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct"); is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged"); is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged"); } -function* pointsCanBeMovedWithKeyboard(widget, win, doc) { +function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) { info("Checking that points respond to keyboard events"); + let singleStep = 3; + let shiftStep = 30; + info("Moving P1 to the left"); + let newOffset = parseInt(widget.p1.style.left) - singleStep; + let x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + let onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 37)); let bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); + + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); info("Moving P1 to the left, fast"); + newOffset = parseInt(widget.p1.style.left) - shiftStep; + x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); info("Moving P1 to the right, fast"); + newOffset = parseInt(widget.p1.style.left) + shiftStep; + x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct"); info("Moving P1 to the bottom"); + newOffset = parseInt(widget.p1.style.top) + singleStep; + let y = widget.bezierCanvas. + offsetsToCoordinates({style: {top: newOffset}})[1]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 40)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); - is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); + is(bezier.P1[1], y, "The new P1 progress coordinate is correct"); info("Moving P1 to the bottom, fast"); + newOffset = parseInt(widget.p1.style.top) + shiftStep; + y = widget.bezierCanvas. + offsetsToCoordinates({style: {top: newOffset}})[1]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); - is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); + is(bezier.P1[1], y, "The new P1 progress coordinate is correct"); info("Moving P1 to the top, fast"); + newOffset = parseInt(widget.p1.style.top) - shiftStep; + y = widget.bezierCanvas. + offsetsToCoordinates({style: {top: newOffset}})[1]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true)); bezier = yield onUpdated; - is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct"); - is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct"); + is(bezier.P1[0], x, "The new P1 time coordinate is correct"); + is(bezier.P1[1], y, "The new P1 progress coordinate is correct"); info("Checking that keyboard events also work with P2"); info("Moving P2 to the left"); + newOffset = parseInt(widget.p2.style.left) - singleStep; + x = widget.bezierCanvas. + offsetsToCoordinates({style: {left: newOffset}})[0]; + onUpdated = widget.once("updated"); widget._onPointKeyDown(getKeyEvent(widget.p2, 37)); bezier = yield onUpdated; - is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct"); + is(bezier.P2[0], x, "The new P2 time coordinate is correct"); is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct"); } function getKeyEvent(target, keyCode, shift=false) { return { target: target, keyCode: keyCode, shiftKey: shift,
--- a/browser/devtools/shared/test/browser_cubic-bezier-03.js +++ b/browser/devtools/shared/test/browser_cubic-bezier-03.js @@ -2,18 +2,19 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Tests that coordinates can be changed programatically in the CubicBezierWidget const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; -const {CubicBezierWidget, PREDEFINED} = +const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets"); add_task(function*() { yield promiseTab("about:blank"); let [host, win, doc] = yield createHost("bottom", TEST_URI); let container = doc.querySelector("#container"); let w = new CubicBezierWidget(container, PREDEFINED.linear);
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_cubic-bezier-04.js @@ -0,0 +1,51 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the CubicBezierPresetWidget generates markup. + +const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; +const {CubicBezierPresetWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets"); + +add_task(function*() { + yield promiseTab("about:blank"); + let [host, win, doc] = yield createHost("bottom", TEST_URI); + + let container = doc.querySelector("#container"); + let w = new CubicBezierPresetWidget(container); + + info("Checking that the presets are created in the parent"); + ok(container.querySelector(".preset-pane"), + "The preset pane has been added"); + + ok(container.querySelector("#preset-categories"), + "The preset categories have been added"); + let categories = container.querySelectorAll(".category"); + is(categories.length, Object.keys(PRESETS).length, + "The preset categories have been added"); + Object.keys(PRESETS).forEach(category => { + ok(container.querySelector("#" + category), `${category} has been added`); + ok(container.querySelector("#preset-category-" + category), + `The preset list for ${category} has been added.`); + }); + + info("Checking that each of the presets and its preview have been added"); + Object.keys(PRESETS).forEach(category => { + Object.keys(PRESETS[category]).forEach(presetLabel => { + let preset = container.querySelector("#" + presetLabel); + ok(preset, `${presetLabel} has been added`); + ok(preset.querySelector("canvas"), + `${presetLabel}'s canvas preview has been added`); + ok(preset.querySelector("p"), + `${presetLabel}'s label has been added`); + }); + }); + + w.destroy(); + host.destroy(); + gBrowser.removeCurrentTab(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_cubic-bezier-05.js @@ -0,0 +1,49 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the CubicBezierPresetWidget cycles menus + +const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; +const {CubicBezierPresetWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = + require("devtools/shared/widgets/CubicBezierPresets"); + +add_task(function*() { + yield promiseTab("about:blank"); + let [host, win, doc] = yield createHost("bottom", TEST_URI); + + let container = doc.querySelector("#container"); + let w = new CubicBezierPresetWidget(container); + + info("Checking that preset is selected if coordinates are known"); + + w.refreshMenu([0, 0, 0, 0]); + is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`), + "The default category is selected"); + is(w._activePreset, null, "There is no selected category"); + + w.refreshMenu(PREDEFINED["linear"]); + is(w.activeCategory, container.querySelector("#ease-in-out"), + "The ease-in-out category is active"); + is(w._activePreset, container.querySelector("#ease-in-out-linear"), + "The ease-in-out-linear preset is active"); + + w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]); + is(w.activeCategory, container.querySelector("#ease-out"), + "The ease-out category is active"); + is(w._activePreset, container.querySelector("#ease-out-sine"), + "The ease-out-sine preset is active"); + + w.refreshMenu([0, 0, 0, 0]); + is(w.activeCategory, container.querySelector("#ease-out"), + "The ease-out category is still active"); + is(w._activePreset, null, "No preset is active"); + + w.destroy(); + host.destroy(); + gBrowser.removeCurrentTab(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_cubic-bezier-06.js @@ -0,0 +1,80 @@ + +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the integration between CubicBezierWidget and CubicBezierPresets + +const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; +const {CubicBezierWidget} = + devtools.require("devtools/shared/widgets/CubicBezierWidget"); +const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets"); + +add_task(function*() { + yield promiseTab("about:blank"); + let [host, win, doc] = yield createHost("bottom", TEST_URI); + + let container = doc.querySelector("#container"); + let w = new CubicBezierWidget(container, + PRESETS["ease-in"]["ease-in-sine"]); + w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]); + + let rect = w.curve.getBoundingClientRect(); + rect.graphTop = rect.height * w.bezierCanvas.padding[0]; + + yield adjustingBezierUpdatesPreset(w, win, doc, rect); + yield selectingPresetUpdatesBezier(w, win, doc, rect); + + w.destroy(); + host.destroy(); + gBrowser.removeCurrentTab(); +}); + +function* adjustingBezierUpdatesPreset(widget, win, doc, rect) { + info("Checking that changing the bezier refreshes the preset menu"); + + is(widget.presets.activeCategory, + doc.querySelector("#ease-in"), + "The selected category is ease-in"); + + is(widget.presets._activePreset, + doc.querySelector("#ease-in-sine"), + "The selected preset is ease-in-sine"); + + info("Generating custom bezier curve by dragging"); + widget._onPointMouseDown({target: widget.p1}); + doc.onmousemove({pageX: rect.left, pageY: rect.graphTop}); + doc.onmouseup(); + + is(widget.presets.activeCategory, + doc.querySelector("#ease-in"), + "The selected category is still ease-in"); + + is(widget.presets._activePreset, null, + "There is no active preset"); + } + +function* selectingPresetUpdatesBezier(widget, win, doc, rect) { + info("Checking that selecting a preset updates bezier curve"); + + info("Listening for the new coordinates event"); + let onNewCoordinates = widget.presets.once("new-coordinates"); + let onUpdated = widget.once("updated"); + + info("Click a preset"); + let preset = doc.querySelector("#ease-in-sine"); + widget.presets._onPresetClick({currentTarget: preset}); + + yield onNewCoordinates; + ok(true, "The preset widget fired the new-coordinates event"); + + let bezier = yield onUpdated; + ok(true, "The bezier canvas fired the updated event"); + + is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct"); + is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct"); + is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct "); + is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct"); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_devices.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let { GetDevices, GetDeviceString, AddDevice } = devtools.require("devtools/shared/devices"); + +add_task(function*() { + Services.prefs.setCharPref("devtools.devices.url", TEST_URI_ROOT + "browser_devices.json"); + + let devices = yield GetDevices(); + + is(devices.TYPES.length, 1, "Found 1 device type."); + + let type1 = devices.TYPES[0]; + + is(devices[type1].length, 2, "Found 2 devices of type #1."); + + let string = GetDeviceString(type1); + ok(typeof string === "string" && string.length > 0, "Able to localize type #1."); + + let device1 = { + name: "SquarePhone", + width: 320, + height: 320, + pixelRatio: 2, + userAgent: "Mozilla/5.0 (Mobile; rv:42.0)", + touch: true, + firefoxOS: true + }; + AddDevice(device1, type1); + devices = yield GetDevices(); + + is(devices[type1].length, 3, "Added new device of type #1."); + ok(devices[type1].filter(d => d.name === device1.name), "Found the new device."); + + let type2 = "appliances"; + let device2 = { + name: "Mr Freezer", + width: 800, + height: 600, + pixelRatio: 5, + userAgent: "Mozilla/5.0 (Appliance; rv:42.0)", + touch: true, + firefoxOS: true + }; + AddDevice(device2, type2); + devices = yield GetDevices(); + + is(devices.TYPES.length, 2, "Added device type #2."); + is(devices[type2].length, 1, "Added new device of type #2."); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_devices.json @@ -0,0 +1,23 @@ +{ + "TYPES": [ "phones" ], + "phones": [ + { + "name": "Small Phone", + "width": 320, + "height": 480, + "pixelRatio": 1, + "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + "touch": true, + "firefoxOS": true + }, + { + "name": "Big Phone", + "width": 360, + "height": 640, + "pixelRatio": 3, + "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0", + "touch": true, + "firefoxOS": true + } + ] +}
--- a/browser/devtools/shared/test/browser_graphs-07a.js +++ b/browser/devtools/shared/test/browser_graphs-07a.js @@ -12,24 +12,28 @@ add_task(function*() { yield performTest(); gBrowser.removeCurrentTab(); }); function* performTest() { let [host, win, doc] = yield createHost(); let graph = new LineGraphWidget(doc.body, "fps"); yield graph.once("ready"); + testGraph(graph, normalDragStop); + yield graph.destroy(); - testGraph(graph); + let graph2 = new LineGraphWidget(doc.body, "fps"); + yield graph2.once("ready"); + testGraph(graph2, buggyDragStop); + yield graph2.destroy(); - yield graph.destroy(); host.destroy(); } -function testGraph(graph) { +function testGraph(graph, dragStop) { graph.setData(TEST_DATA); info("Making a selection."); dragStart(graph, 300); ok(graph.hasSelectionInProgress(), "The selection should start (1)."); is(graph.getSelection().start, 300, @@ -181,21 +185,32 @@ function click(graph, x, y = 1) { function dragStart(graph, x, y = 1) { x /= window.devicePixelRatio; y /= window.devicePixelRatio; graph._onMouseMove({ clientX: x, clientY: y }); graph._onMouseDown({ clientX: x, clientY: y }); } -function dragStop(graph, x, y = 1) { +function normalDragStop(graph, x, y = 1) { x /= window.devicePixelRatio; y /= window.devicePixelRatio; graph._onMouseMove({ clientX: x, clientY: y }); graph._onMouseUp({ clientX: x, clientY: y }); } +function buggyDragStop(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + + // Only fire a mousemove instead of a mouseup. + // This happens when the mouseup happens outside of the toolbox, + // see Bug 1066504. + graph._onMouseMove({ clientX: x, clientY: y }); + graph._onMouseMove({ clientX: x, clientY: y, buttons: 0 }); +} + function scroll(graph, wheel, x, y = 1) { x /= window.devicePixelRatio; y /= window.devicePixelRatio; graph._onMouseMove({ clientX: x, clientY: y }); graph._onMouseWheel({ clientX: x, clientY: y, detail: wheel }); }
--- a/browser/devtools/shared/test/unit/test_bezierCanvas.js +++ b/browser/devtools/shared/test/unit/test_bezierCanvas.js @@ -99,15 +99,18 @@ function getCanvasMock(w=200, h=400) { clearRect: () => {}, beginPath: () => {}, closePath: () => {}, moveTo: () => {}, lineTo: () => {}, stroke: () => {}, arc: () => {}, fill: () => {}, - bezierCurveTo: () => {} + bezierCurveTo: () => {}, + save: () => {}, + restore: () => {}, + setTransform: () => {} }; }, width: w, height: h }; }
--- a/browser/devtools/shared/test/unit/test_cubicBezier.js +++ b/browser/devtools/shared/test/unit/test_cubicBezier.js @@ -14,16 +14,17 @@ let {CubicBezier} = require("devtools/sh function run_test() { throwsWhenMissingCoordinates(); throwsWhenIncorrectCoordinates(); convertsStringCoordinates(); coordinatesToStringOutputsAString(); pointGettersReturnPointCoordinatesArrays(); toStringOutputsCubicBezierValue(); + toStringOutputsCssPresetValues(); } function throwsWhenMissingCoordinates() { do_check_throws(() => { new CubicBezier(); }, "Throws an exception when coordinates are missing"); } @@ -79,18 +80,37 @@ function pointGettersReturnPointCoordina do_check_eq(c.P1[1], .2); do_check_eq(c.P2[0], .5); do_check_eq(c.P2[1], 1); } function toStringOutputsCubicBezierValue() { do_print("toString() outputs the cubic-bezier() value"); + let c = new CubicBezier([0, 1, 1, 0]); + do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)"); +} + +function toStringOutputsCssPresetValues() { + do_print("toString() outputs the css predefined values"); + let c = new CubicBezier([0, 0, 1, 1]); - do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)"); + do_check_eq(c.toString(), "linear"); + + c = new CubicBezier([0.25, 0.1, 0.25, 1]); + do_check_eq(c.toString(), "ease"); + + c = new CubicBezier([0.42, 0, 1, 1]); + do_check_eq(c.toString(), "ease-in"); + + c = new CubicBezier([0, 0, 0.58, 1]); + do_check_eq(c.toString(), "ease-out"); + + c = new CubicBezier([0.42, 0, 0.58, 1]); + do_check_eq(c.toString(), "ease-in-out"); } function do_check_throws(cb, info) { do_print(info); let hasThrown = false; try { cb();
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/widgets/CubicBezierPresets.js @@ -0,0 +1,64 @@ +/** + * 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/. + */ + +// Set of preset definitions for use with CubicBezierWidget +// Credit: http://easings.net + +"use strict"; + +const PREDEFINED = { + "ease": [0.25, 0.1, 0.25, 1], + "linear": [0, 0, 1, 1], + "ease-in": [0.42, 0, 1, 1], + "ease-out": [0, 0, 0.58, 1], + "ease-in-out": [0.42, 0, 0.58, 1] +}; + +const PRESETS = { + "ease-in": { + "ease-in-linear": [0, 0, 1, 1], + "ease-in-ease-in": [0.42, 0, 1, 1], + "ease-in-sine": [0.47, 0, 0.74, 0.71], + "ease-in-quadratic": [0.55, 0.09, 0.68, 0.53], + "ease-in-cubic": [0.55, 0.06, 0.68, 0.19], + "ease-in-quartic": [0.9, 0.03, 0.69, 0.22], + "ease-in-quintic": [0.76, 0.05, 0.86, 0.06], + "ease-in-exponential": [0.95, 0.05, 0.8, 0.04], + "ease-in-circular": [0.6, 0.04, 0.98, 0.34], + "ease-in-backward": [0.6, -0.28, 0.74, 0.05] + }, + "ease-out": { + "ease-out-linear": [0, 0, 1, 1], + "ease-out-ease-out": [0, 0, 0.58, 1], + "ease-out-sine": [0.39, 0.58, 0.57, 1], + "ease-out-quadratic": [0.25, 0.46, 0.45, 0.94], + "ease-out-cubic": [0.22, 0.61, 0.36, 1], + "ease-out-quartic": [0.17, 0.84, 0.44, 1], + "ease-out-quintic": [0.23, 1, 0.32, 1], + "ease-out-exponential": [0.19, 1, 0.22, 1], + "ease-out-circular": [0.08, 0.82, 0.17, 1], + "ease-out-backward": [0.18, 0.89, 0.32, 1.28] + }, + "ease-in-out": { + "ease-in-out-linear": [0, 0, 1, 1], + "ease-in-out-ease": [0.25, 0.1, 0.25, 1], + "ease-in-out-ease-in-out": [0.42, 0, 0.58, 1], + "ease-in-out-sine": [0.45, 0.05, 0.55, 0.95], + "ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96], + "ease-in-out-cubic": [0.65, 0.05, 0.36, 1], + "ease-in-out-quartic": [0.77, 0, 0.18, 1], + "ease-in-out-quintic": [0.86, 0, 0.07, 1], + "ease-in-out-exponential": [1, 0, 0, 1], + "ease-in-out-circular": [0.79, 0.14, 0.15, 0.86], + "ease-in-out-backward": [0.68, -0.55, 0.27, 1.55] + } +}; + +const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0]; + +exports.PRESETS = PRESETS; +exports.PREDEFINED = PREDEFINED; +exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;
--- a/browser/devtools/shared/widgets/CubicBezierWidget.js +++ b/browser/devtools/shared/widgets/CubicBezierWidget.js @@ -22,24 +22,17 @@ // Based on www.cubic-bezier.com by Lea Verou // See https://github.com/LeaVerou/cubic-bezier "use strict"; const EventEmitter = require("devtools/toolkit/event-emitter"); const {setTimeout, clearTimeout} = require("sdk/timers"); - -const PREDEFINED = exports.PREDEFINED = { - "ease": [.25, .1, .25, 1], - "linear": [0, 0, 1, 1], - "ease-in": [.42, 0, 1, 1], - "ease-out": [0, 0, .58, 1], - "ease-in-out": [.42, 0, .58, 1] -}; +const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = require("devtools/shared/widgets/CubicBezierPresets"); /** * CubicBezier data structure helper * Accepts an array of coordinates and exposes a few useful getters * @param {Array} coordinates i.e. [.42, 0, .58, 1] */ function CubicBezier(coordinates) { if (!coordinates) { @@ -54,32 +47,36 @@ function CubicBezier(coordinates) { throw "Wrong coordinate at " + i + "(" + xy + ")"; } } this.coordinates.toString = function() { return this.map(n => { return (Math.round(n * 100)/100 + '').replace(/^0\./, '.'); }) + ""; - } + }; } exports.CubicBezier = CubicBezier; CubicBezier.prototype = { get P1() { return this.coordinates.slice(0, 2); }, get P2() { return this.coordinates.slice(2); }, toString: function() { - return 'cubic-bezier(' + this.coordinates + ')'; + // Check first if current coords are one of css predefined functions + let predefName = Object.keys(PREDEFINED) + .find(key => coordsAreEqual(PREDEFINED[key], this.coordinates)); + + return predefName || 'cubic-bezier(' + this.coordinates + ')'; } }; /** * Bezier curve canvas plotting class * @param {DOMNode} canvas * @param {CubicBezier} bezier * @param {Array} padding Amount of horizontal,vertical padding around the graph @@ -92,17 +89,17 @@ function BezierCanvas(canvas, bezier, pa // Convert to a cartesian coordinate system with axes from 0 to 1 this.ctx = this.canvas.getContext('2d'); let p = this.padding; this.ctx.scale(canvas.width * (1 - p[1] - p[3]), -canvas.height * (1 - p[0] - p[2])); this.ctx.translate(p[3] / (1 - p[1] - p[3]), -1 - p[0] / (1 - p[0] - p[2])); -}; +} exports.BezierCanvas = BezierCanvas; BezierCanvas.prototype = { /** * Get P1 and P2 current top/left offsets so they can be positioned * @return {Array} Returns an array of 2 {top:String,left:String} objects */ @@ -110,78 +107,86 @@ BezierCanvas.prototype = { let p = this.padding, w = this.canvas.width, h = this.canvas.height; return [{ left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px', top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px' }, { left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px', top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px' - }] + }]; }, /** * Convert an element's left/top offsets into coordinates */ offsetsToCoordinates: function(element) { let p = this.padding, w = this.canvas.width, h = this.canvas.height; // Convert padding percentage to actual padding p = p.map(function(a, i) { return a * (i % 2? w : h)}); return [ - (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]), - (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2]) + (parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]), + (h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2]) ]; }, /** * Draw the cubic bezier curve for the current coordinates */ plot: function(settings={}) { let xy = this.bezier.coordinates; let defaultSettings = { handleColor: '#666', handleThickness: .008, bezierColor: '#4C9ED9', - bezierThickness: .015 + bezierThickness: .015, + drawHandles: true }; for (let setting in settings) { defaultSettings[setting] = settings[setting]; } - this.ctx.clearRect(-.5,-.5, 2, 2); + // Clear the canvas –making sure to clear the + // whole area by resetting the transform first. + this.ctx.save(); + this.ctx.setTransform(1, 0, 0, 1, 0, 0); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.restore(); - // Draw control handles - this.ctx.beginPath(); - this.ctx.fillStyle = defaultSettings.handleColor; - this.ctx.lineWidth = defaultSettings.handleThickness; - this.ctx.strokeStyle = defaultSettings.handleColor; - - this.ctx.moveTo(0, 0); - this.ctx.lineTo(xy[0], xy[1]); - this.ctx.moveTo(1,1); - this.ctx.lineTo(xy[2], xy[3]); + if (defaultSettings.drawHandles) { + // Draw control handles + this.ctx.beginPath(); + this.ctx.fillStyle = defaultSettings.handleColor; + this.ctx.lineWidth = defaultSettings.handleThickness; + this.ctx.strokeStyle = defaultSettings.handleColor; - this.ctx.stroke(); - this.ctx.closePath(); + this.ctx.moveTo(0, 0); + this.ctx.lineTo(xy[0], xy[1]); + this.ctx.moveTo(1,1); + this.ctx.lineTo(xy[2], xy[3]); + + this.ctx.stroke(); + this.ctx.closePath(); - function circle(ctx, cx, cy, r) { - return ctx.beginPath(); - ctx.arc(cx, cy, r, 0, 2*Math.PI, !1); - ctx.closePath(); + var circle = function(ctx, cx, cy, r) { + return ctx.beginPath(); + ctx.arc(cx, cy, r, 0, 2*Math.PI, !1); + ctx.closePath(); + }; + + circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness); + this.ctx.fill(); + circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness); + this.ctx.fill(); } - circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness); - this.ctx.fill(); - circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness); - this.ctx.fill(); - // Draw bezier curve this.ctx.beginPath(); this.ctx.lineWidth = defaultSettings.bezierThickness; this.ctx.strokeStyle = defaultSettings.bezierColor; this.ctx.moveTo(0,0); this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1); this.ctx.stroke(); this.ctx.closePath(); @@ -192,104 +197,118 @@ BezierCanvas.prototype = { * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and * adds the control points and user interaction * @param {DOMNode} parent The container where the graph should be created * @param {Array} coordinates Coordinates of the curve to be drawn * * Emits "updated" events whenever the curve is changed. Along with the event is * sent a CubicBezier object */ -function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) { +function CubicBezierWidget(parent, coordinates=PRESETS["ease-in"]["ease-in-sine"]) { + EventEmitter.decorate(this); + this.parent = parent; let {curve, p1, p2} = this._initMarkup(); + this.curveBoundingBox = curve.getBoundingClientRect(); this.curve = curve; - this.curveBoundingBox = curve.getBoundingClientRect(); this.p1 = p1; this.p2 = p2; // Create and plot the bezier curve this.bezierCanvas = new BezierCanvas(this.curve, - new CubicBezier(coordinates), [.25, 0]); + new CubicBezier(coordinates), [0.30, 0]); this.bezierCanvas.plot(); // Place the control points let offsets = this.bezierCanvas.offsets; this.p1.style.left = offsets[0].left; this.p1.style.top = offsets[0].top; this.p2.style.left = offsets[1].left; this.p2.style.top = offsets[1].top; this._onPointMouseDown = this._onPointMouseDown.bind(this); this._onPointKeyDown = this._onPointKeyDown.bind(this); this._onCurveClick = this._onCurveClick.bind(this); - this._initEvents(); + this._onNewCoordinates = this._onNewCoordinates.bind(this); + + // Add preset preview menu + this.presets = new CubicBezierPresetWidget(parent); // Add the timing function previewer this.timingPreview = new TimingFunctionPreviewWidget(parent); - EventEmitter.decorate(this); + this._initEvents(); } exports.CubicBezierWidget = CubicBezierWidget; CubicBezierWidget.prototype = { _initMarkup: function() { let doc = this.parent.ownerDocument; + let wrap = doc.createElement("div"); + wrap.className = "display-wrap"; + let plane = doc.createElement("div"); plane.className = "coordinate-plane"; let p1 = doc.createElement("button"); p1.className = "control-point"; p1.id = "P1"; plane.appendChild(p1); let p2 = doc.createElement("button"); p2.className = "control-point"; p2.id = "P2"; plane.appendChild(p2); let curve = doc.createElement("canvas"); - curve.setAttribute("height", "400"); - curve.setAttribute("width", "200"); + curve.setAttribute("width", 150); + curve.setAttribute("height", 370); curve.id = "curve"; - plane.appendChild(curve); - this.parent.appendChild(plane); + plane.appendChild(curve); + wrap.appendChild(plane); + + this.parent.appendChild(wrap); return { p1: p1, p2: p2, curve: curve - } + }; }, _removeMarkup: function() { - this.parent.ownerDocument.querySelector(".coordinate-plane").remove(); + this.parent.ownerDocument.querySelector(".display-wrap").remove(); }, _initEvents: function() { this.p1.addEventListener("mousedown", this._onPointMouseDown); this.p2.addEventListener("mousedown", this._onPointMouseDown); this.p1.addEventListener("keydown", this._onPointKeyDown); this.p2.addEventListener("keydown", this._onPointKeyDown); this.curve.addEventListener("click", this._onCurveClick); + + this.presets.on("new-coordinates", this._onNewCoordinates); }, _removeEvents: function() { this.p1.removeEventListener("mousedown", this._onPointMouseDown); this.p2.removeEventListener("mousedown", this._onPointMouseDown); this.p1.removeEventListener("keydown", this._onPointKeyDown); this.p2.removeEventListener("keydown", this._onPointKeyDown); this.curve.removeEventListener("click", this._onCurveClick); + + this.presets.off("new-coordinates", this._onNewCoordinates); }, _onPointMouseDown: function(event) { // Updating the boundingbox in case it has changed this.curveBoundingBox = this.curve.getBoundingClientRect(); let point = event.target; let doc = point.ownerDocument; @@ -312,17 +331,17 @@ CubicBezierWidget.prototype = { point.style.top = y - top + "px"; self._updateFromPoints(); }; doc.onmouseup = function () { point.focus(); doc.onmousemove = doc.onmouseup = null; - } + }; }, _onPointKeyDown: function(event) { let point = event.target; let code = event.keyCode; if (code >= 37 && code <= 40) { event.preventDefault(); @@ -339,16 +358,18 @@ CubicBezierWidget.prototype = { case 40: point.style.top = top + offset + 'px'; break; } this._updateFromPoints(); } }, _onCurveClick: function(event) { + this.curveBoundingBox = this.curve.getBoundingClientRect(); + let left = this.curveBoundingBox.left; let top = this.curveBoundingBox.top; let x = event.pageX - left; let y = event.pageY - top; // Find which point is closer let distP1 = distance(x, y, parseInt(this.p1.style.left), parseInt(this.p1.style.top)); @@ -357,24 +378,29 @@ CubicBezierWidget.prototype = { let point = distP1 < distP2 ? this.p1 : this.p2; point.style.left = x + "px"; point.style.top = y + "px"; this._updateFromPoints(); }, + _onNewCoordinates: function(event, coordinates) { + this.coordinates = coordinates; + }, + /** * Get the current point coordinates and redraw the curve to match */ _updateFromPoints: function() { // Get the new coordinates from the point's offsets - let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1) + let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1); coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2)); + this.presets.refreshMenu(coordinates); this._redraw(coordinates); }, /** * Redraw the curve * @param {Array} coordinates The array of control point coordinates */ _redraw: function(coordinates) { @@ -386,17 +412,17 @@ CubicBezierWidget.prototype = { this.timingPreview.preview(this.bezierCanvas.bezier + ""); }, /** * Set new coordinates for the control points and redraw the curve * @param {Array} coordinates */ set coordinates(coordinates) { - this._redraw(coordinates) + this._redraw(coordinates); // Move the points let offsets = this.bezierCanvas.offsets; this.p1.style.left = offsets[0].left; this.p1.style.top = offsets[0].top; this.p2.style.left = offsets[1].left; this.p2.style.top = offsets[1].top; }, @@ -415,30 +441,282 @@ CubicBezierWidget.prototype = { // Try with one of the predefined values let coordinates = PREDEFINED[value]; // Otherwise parse the coordinates from the cubic-bezier function if (!coordinates && value.startsWith("cubic-bezier")) { coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat); } + this.presets.refreshMenu(coordinates); this.coordinates = coordinates; }, destroy: function() { this._removeEvents(); this._removeMarkup(); this.timingPreview.destroy(); + this.presets.destroy(); this.curve = this.p1 = this.p2 = null; } }; /** + * CubicBezierPreset widget. + * Builds a menu of presets from CubicBezierPresets + * @param {DOMNode} parent The container where the preset panel should be created + * + * Emits "new-coordinate" event along with the coordinates + * whenever a preset is selected. + */ +function CubicBezierPresetWidget(parent) { + this.parent = parent; + + let {presetPane, presets, categories} = this._initMarkup(); + this.presetPane = presetPane; + this.presets = presets; + this.categories = categories; + + this._activeCategory = null; + this._activePresetList = null; + this._activePreset = null; + + this._onCategoryClick = this._onCategoryClick.bind(this); + this._onPresetClick = this._onPresetClick.bind(this); + + EventEmitter.decorate(this); + this._initEvents(); +} + +exports.CubicBezierPresetWidget = CubicBezierPresetWidget; + +CubicBezierPresetWidget.prototype = { + /* + * Constructs a list of all preset categories and a list + * of presets for each category. + * + * High level markup: + * div .preset-pane + * div .preset-categories + * div .category + * div .category + * ... + * div .preset-container + * div .presetList + * div .preset + * ... + * div .presetList + * div .preset + * ... + */ + _initMarkup: function() { + let doc = this.parent.ownerDocument; + + let presetPane = doc.createElement("div"); + presetPane.className = "preset-pane"; + + let categoryList = doc.createElement("div"); + categoryList.id = "preset-categories"; + + let presetContainer = doc.createElement("div"); + presetContainer.id = "preset-container"; + + Object.keys(PRESETS).forEach(categoryLabel => { + let category = this._createCategory(categoryLabel); + categoryList.appendChild(category); + + let presetList = this._createPresetList(categoryLabel); + presetContainer.appendChild(presetList); + }); + + presetPane.appendChild(categoryList); + presetPane.appendChild(presetContainer); + + this.parent.appendChild(presetPane); + + let allCategories = presetPane.querySelectorAll(".category"); + let allPresets = presetPane.querySelectorAll(".preset"); + + return { + presetPane: presetPane, + presets: allPresets, + categories: allCategories + }; + }, + + _createCategory: function(categoryLabel) { + let doc = this.parent.ownerDocument; + + let category = doc.createElement("div"); + category.id = categoryLabel; + category.classList.add("category"); + + let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel); + category.textContent = categoryDisplayLabel; + + return category; + }, + + _normalizeCategoryLabel: function(categoryLabel) { + return categoryLabel.replace("/-/g", " "); + }, + + _createPresetList: function(categoryLabel) { + let doc = this.parent.ownerDocument; + + let presetList = doc.createElement("div"); + presetList.id = "preset-category-" + categoryLabel; + presetList.classList.add("preset-list"); + + Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => { + let preset = this._createPreset(categoryLabel, presetLabel); + presetList.appendChild(preset); + }); + + return presetList; + }, + + _createPreset: function(categoryLabel, presetLabel) { + let doc = this.parent.ownerDocument; + + let preset = doc.createElement("div"); + preset.classList.add("preset"); + preset.id = presetLabel; + preset.coordinates = PRESETS[categoryLabel][presetLabel]; + + // Create preset preview + let curve = doc.createElement("canvas"); + let bezier = new CubicBezier(preset.coordinates); + + curve.setAttribute("height", 55); + curve.setAttribute("width", 55); + + preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]); + preset.bezierCanvas.plot({ + drawHandles: false, + bezierThickness: 0.025 + }); + + preset.appendChild(curve); + + // Create preset label + let presetLabelElem = doc.createElement("p"); + let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel); + presetLabelElem.textContent = presetDisplayLabel; + preset.appendChild(presetLabelElem); + + return preset; + }, + + _normalizePresetLabel: function(categoryLabel, presetLabel) { + return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " "); + }, + + _initEvents: function() { + for (let category of this.categories) { + category.addEventListener("click", this._onCategoryClick); + } + + for (let preset of this.presets) { + preset.addEventListener("click", this._onPresetClick); + } + }, + + _removeEvents: function() { + for (let category of this.categories) { + category.removeEventListener("click", this._onCategoryClick); + } + + for (let preset of this.presets) { + preset.removeEventListener("click", this._onPresetClick); + } + }, + + _onPresetClick: function(event) { + this.emit("new-coordinates", event.currentTarget.coordinates); + this.activePreset = event.currentTarget; + }, + + _onCategoryClick: function(event) { + this.activeCategory = event.target; + }, + + _setActivePresetList: function(presetListId) { + let presetList = this.presetPane.querySelector("#" + presetListId); + swapClassName("active-preset-list", this._activePresetList, presetList); + this._activePresetList = presetList; + }, + + set activeCategory(category) { + swapClassName("active-category", this._activeCategory, category); + this._activeCategory = category; + this._setActivePresetList("preset-category-" + category.id); + }, + + get activeCategory() { + return this._activeCategory; + }, + + set activePreset(preset) { + swapClassName("active-preset", this._activePreset, preset); + this._activePreset = preset; + }, + + get activePreset() { + return this._activePreset; + }, + + /** + * Called by CubicBezierWidget onload and when + * the curve is modified via the canvas. + * Attempts to match the new user setting with an + * existing preset. + * @param {Array} coordinates new coords [i, j, k, l] + */ + refreshMenu: function(coordinates) { + // If we cannot find a matching preset, keep + // menu on last known preset category. + let category = this._activeCategory; + + // If we cannot find a matching preset + // deselect any selected preset. + let preset = null; + + // If a category has never been viewed before + // show the default category. + if (!category) { + category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY); + } + + // If the new coordinates do match a preset, + // set its category and preset button as active. + Object.keys(PRESETS).forEach(categoryLabel => { + + Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => { + if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) { + category = this.parent.querySelector("#" + categoryLabel); + preset = this.parent.querySelector("#" + presetLabel); + } + }); + + }); + + this.activeCategory = category; + this.activePreset = preset; + }, + + destroy: function() { + this._removeEvents(); + this.parent.querySelector(".preset-pane").remove(); + } +}; + +/** * The TimingFunctionPreviewWidget animates a dot on a scale with a given * timing-function * @param {DOMNode} parent The container where this widget should go */ function TimingFunctionPreviewWidget(parent) { this.previousValue = null; this.autoRestartAnimation = null; @@ -549,8 +827,34 @@ function isValidTimingFunction(value) { // Or it has to match a cubic-bezier expression if (value.match(/^cubic-bezier\(([0-9.\- ]+,){3}[0-9.\- ]+\)/)) { return true; } return false; } + +/** + * Removes a class from a node and adds it to another. + * @param {String} className the class to swap + * @param {DOMNode} from the node to remove the class from + * @param {DOMNode} to the node to add the class to + */ +function swapClassName(className, from, to) { + if (from !== null) { + from.classList.remove(className); + } + + if (to !== null) { + to.classList.add(className); + } +} + +/** + * Compares two arrays of coordinates [i, j, k, l] + * @param {Array} c1 first coordinate array to compare + * @param {Array} c2 second coordinate array to compare + * @return {Boolean} + */ +function coordsAreEqual(c1, c2) { + return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true); +}
--- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -176,16 +176,17 @@ this.AbstractCanvasGraph = function(pare this._height = canvas.height = bounds.height * this._pixelRatio; this._ctx = canvas.getContext("2d"); this._ctx.mozImageSmoothingEnabled = false; this._cursor = new GraphCursor(); this._selection = new GraphArea(); this._selectionDragger = new GraphAreaDragger(); this._selectionResizer = new GraphAreaResizer(); + this._isMouseActive = false; this._onAnimationFrame = this._onAnimationFrame.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseDown = this._onMouseDown.bind(this); this._onMouseUp = this._onMouseUp.bind(this); this._onMouseWheel = this._onMouseWheel.bind(this); this._onMouseOut = this._onMouseOut.bind(this); this._onResize = this._onResize.bind(this); @@ -947,31 +948,40 @@ AbstractCanvasGraph.prototype = { return { left: x, top: y }; }, /** * Listener for the "mousemove" event on the graph's container. */ _onMouseMove: function(e) { + let resizer = this._selectionResizer; + let dragger = this._selectionDragger; + + // If a mouseup happened outside the toolbox and the current operation + // is causing the selection changed, then end it. + if (e.buttons == 0 && (this.hasSelectionInProgress() || + resizer.margin != null || + dragger.origin != null)) { + return this._onMouseUp(e); + } + let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; let mouseY = (e.clientY - offset.top) * this._pixelRatio; this._cursor.x = mouseX; this._cursor.y = mouseY; - let resizer = this._selectionResizer; if (resizer.margin != null) { this._selection[resizer.margin] = mouseX; this._shouldRedraw = true; this.emit("selecting"); return; } - let dragger = this._selectionDragger; if (dragger.origin != null) { this._selection.start = dragger.anchor.start - dragger.origin + mouseX; this._selection.end = dragger.anchor.end - dragger.origin + mouseX; this._shouldRedraw = true; this.emit("selecting"); return; } @@ -1008,16 +1018,17 @@ AbstractCanvasGraph.prototype = { this._shouldRedraw = true; }, /** * Listener for the "mousedown" event on the graph's container. */ _onMouseDown: function(e) { + this._isMouseActive = true; let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; switch (this._canvas.getAttribute("input")) { case "hovering-background": case "hovering-region": if (!this.selectionEnabled) { break; @@ -1046,16 +1057,17 @@ AbstractCanvasGraph.prototype = { this._shouldRedraw = true; this.emit("mousedown"); }, /** * Listener for the "mouseup" event on the graph's container. */ _onMouseUp: function(e) { + this._isMouseActive = false; let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; switch (this._canvas.getAttribute("input")) { case "hovering-background": case "hovering-region": if (!this.selectionEnabled) { break; @@ -1156,31 +1168,27 @@ AbstractCanvasGraph.prototype = { selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2; } this._shouldRedraw = true; this.emit("selecting"); this.emit("scroll"); }, - /** + /** * Listener for the "mouseout" event on the graph's container. + * Clear any active cursors if a drag isn't happening. */ - _onMouseOut: function() { - if (this.hasSelectionInProgress()) { - this.dropSelection(); + _onMouseOut: function(e) { + if (!this._isMouseActive) { + this._cursor.x = null; + this._cursor.y = null; + this._canvas.removeAttribute("input"); + this._shouldRedraw = true; } - - this._cursor.x = null; - this._cursor.y = null; - this._selectionResizer.margin = null; - this._selectionDragger.origin = null; - - this._canvas.removeAttribute("input"); - this._shouldRedraw = true; }, /** * Listener for the "resize" event on the graph's parent node. */ _onResize: function() { if (this.hasData()) { setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
--- a/browser/devtools/shared/widgets/Tooltip.js +++ b/browser/devtools/shared/widgets/Tooltip.js @@ -790,18 +790,18 @@ Tooltip.prototype = { * the instance of the widget */ setCubicBezierContent: function(bezier) { let def = promise.defer(); // Create an iframe to host the cubic-bezier widget let iframe = this.doc.createElementNS(XHTML_NS, "iframe"); iframe.setAttribute("transparent", true); - iframe.setAttribute("width", "200"); - iframe.setAttribute("height", "415"); + iframe.setAttribute("width", "410"); + iframe.setAttribute("height", "360"); iframe.setAttribute("flex", "1"); iframe.setAttribute("class", "devtools-tooltip-iframe"); let panel = this.panel; let xulWin = this.doc.ownerGlobal; // Wait for the load to initialize the widget function onLoad() {
--- a/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml +++ b/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml @@ -3,23 +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/. --> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/> - <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/> + <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" type="text/css"/> <script type="application/javascript;version=1.8" src="theme-switching.js"/> <style> - body { + html, body { margin: 0; padding: 0; - width: 200px; - height: 415px; + overflow: hidden; + width: 410px; + height: 370px; } </style> </head> <body role="application"> <div id="container"></div> </body> </html>
--- a/browser/devtools/shared/widgets/cubic-bezier.css +++ b/browser/devtools/shared/widgets/cubic-bezier.css @@ -1,41 +1,37 @@ /* 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/. */ /* Based on Lea Verou www.cubic-bezier.com See https://github.com/LeaVerou/cubic-bezier */ -.coordinate-plane { - position: absolute; - line-height: 0; - height: 400px; - width: 200px; +#container { + display: flex; + width: 410px; + height: 370px; + flex-direction: row-reverse; + overflow: hidden; } -.coordinate-plane:before, -.coordinate-plane:after { - position: absolute; - bottom: 25%; - left: 0; - width: 100%; +.display-wrap { + width: 50%; + height: 100%; + text-align: center; + overflow: hidden; } -.coordinate-plane:before { - content: ""; - border-bottom: 2px solid; - transform: rotate(-90deg) translateY(2px); - transform-origin: bottom left; -} +/* Coordinate Plane */ -.coordinate-plane:after { - content: ""; - border-top: 2px solid; - margin-bottom: -2px; +.coordinate-plane { + width: 150px; + height: 370px; + margin: 0 auto; + position: relative; } .theme-dark .coordinate-plane:before, .theme-dark .coordinate-plane:after { border-color: #eee; } .control-point { @@ -45,64 +41,60 @@ width: 10px; border: 0; background: #666; display: block; margin: -5px 0 0 -5px; outline: none; border-radius: 5px; padding: 0; - cursor: pointer; } -#P1x, #P1y { - color: #f08; -} - -#P2x, #P2y { - color: #0ab; -} - -canvas#curve { - background: - linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat, - repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat, - repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat; - - background-size: 100% 50%, 100% 50%, 100% 50%; - background-position: 25%, 0, 0; +.display-wrap { + background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat; + background-size: 100% 100%, 100% 100%; + background-position: -2px 5px, -2px 5px; -moz-user-select: none; } -.theme-dark canvas#curve { - background: - linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat, - repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat, - repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat; +.theme-dark .display-wrap { + background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat; + background-size: 100% 100%, 100% 100%; + background-position: -2px 5px, -2px 5px; - background-size: 100% 50%, 100% 50%, 100% 50%; - background-position: 25%, 0, 0; + -moz-user-select: none; +} +canvas#curve { + background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat; + background-size: 100% 100%; + background-position: 0 0; } -/* Timing function preview widget */ +.theme-dark canvas#curve { + background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat; +} + +/* Timing Function Preview Widget */ .timing-function-preview { position: absolute; - top: 400px; + bottom: 20px; + right: 27px; + width: 150px; } .timing-function-preview .scale { position: absolute; top: 6px; left: 0; z-index: 1; - width: 200px; + width: 150px; height: 1px; background: #ccc; } .timing-function-preview .dot { position: absolute; top: 0; @@ -123,20 +115,126 @@ canvas#curve { animation-name: timing-function-preview; } @keyframes timing-function-preview { 0% { left: -7px; } 33% { - left: 193px; + left: 143px; } 50% { - left: 193px; + left: 143px; } 83% { left: -7px; } 100% { left: -7px; } } + +/* Preset Widget */ + +.preset-pane { + width:50%; + height: 100%; + border-right: 1px solid var(--theme-splitter-color); +} + +#preset-categories { + display: flex; + width: 94%; + border: 1px solid var(--theme-splitter-color); + border-radius: 2px; + background-color: var(--theme-toolbar-background); + margin-left: 4px; + margin-top: 3px; +} + +#preset-categories .category:last-child { + border-right: none; +} + +.category { + flex: 1 1 auto; + padding: 5px; + width: 33.33%; + text-align: center; + text-transform: capitalize; + border-right: 1px solid var(--theme-splitter-color); + cursor: default; + color: var(--theme-body-color); +} + +.category:hover { + background-color: var(--theme-tab-toolbar-background); +} + +.active-category { + background-color: var(--theme-selection-background); + color: var(--theme-selection-color); +} + +.active-category:hover { + background-color: var(--theme-selection-background); +} + +#preset-container { + padding: 0px; + width: 100%; + height: 331px; + overflow-y: scroll; +} + +.preset-list { + display: none; +} + +.active-preset-list { + display: flex; + flex-wrap: wrap; + justify-content: left; + padding-left: 4px; + padding-top: 3px; +} + +.preset { + cursor: pointer; + width: 55px; + margin: 5px 11px 0px 0px; + text-transform: capitalize; + text-align: center; +} + +.preset canvas { + display: block; + border: 1px solid #ccc; + border-radius: 3px; + background-color: var(--theme-body-background); +} + +.theme-dark .preset canvas { + border-color: #444e58; +} + +.preset p { + text-align: center; + font-size: 0.9em; + line-height: 0px; + margin: 2px 0px 0px 0p; + color: var(--theme-body-color-alt); +} + +.active-preset p, .active-preset:hover p { + color: var(--theme-body-color); +} + +.preset:hover canvas { + border-color: var(--theme-selection-background); +} + +.active-preset canvas, .active-preset:hover canvas, +.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas { + background-color: var(--theme-selection-background-semitransparent); + border-color: var(--theme-selection-background); +}
--- a/browser/devtools/webide/content/newapp.js +++ b/browser/devtools/webide/content/newapp.js @@ -10,36 +10,37 @@ Cu.import("resource://gre/modules/Servic Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm"); const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm"); const {AppProjects} = require("devtools/app-manager/app-projects"); -const APP_CREATOR_LIST = "devtools.webide.templatesURL"; const {AppManager} = require("devtools/webide/app-manager"); -const {GetTemplatesJSON} = require("devtools/webide/remote-resources"); +const {getJSON} = require("devtools/shared/getjson"); + +const TEMPLATES_URL = "devtools.webide.templatesURL"; let gTemplateList = null; // See bug 989619 console.log = console.log.bind(console); console.warn = console.warn.bind(console); console.error = console.error.bind(console); window.addEventListener("load", function onLoad() { window.removeEventListener("load", onLoad); let projectNameNode = document.querySelector("#project-name"); projectNameNode.addEventListener("input", canValidate, true); - getJSON(); + getTemplatesJSON(); }, true); -function getJSON() { - GetTemplatesJSON().then(list => { +function getTemplatesJSON() { + getJSON(TEMPLATES_URL).then(list => { if (!Array.isArray(list)) { throw new Error("JSON response not an array"); } if (list.length == 0) { throw new Error("JSON response is an empty array"); } gTemplateList = list; let templatelistNode = document.querySelector("#templatelist");
--- a/browser/devtools/webide/content/webide.js +++ b/browser/devtools/webide/content/webide.js @@ -14,33 +14,34 @@ const {require} = devtools; const {Services} = Cu.import("resource://gre/modules/Services.jsm"); const {AppProjects} = require("devtools/app-manager/app-projects"); const {Connection} = require("devtools/client/connection-manager"); const {AppManager} = require("devtools/webide/app-manager"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); const ProjectEditor = require("projecteditor/projecteditor"); const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm"); const {GetAvailableAddons} = require("devtools/webide/addons"); -const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources"); +const {getJSON} = require("devtools/shared/getjson"); const utils = require("devtools/webide/utils"); const Telemetry = require("devtools/shared/telemetry"); const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes"); const {showDoorhanger} = require("devtools/shared/doorhanger"); const ProjectList = require("devtools/webide/project-list"); const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties"); const HTML = "http://www.w3.org/1999/xhtml"; const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting"; const MAX_ZOOM = 1.4; const MIN_ZOOM = 0.6; -// download template index early -GetTemplatesJSON(true); +// Download remote resources early +getJSON("devtools.webide.addonsURL", true); +getJSON("devtools.webide.templatesURL", true); // See bug 989619 console.log = console.log.bind(console); console.warn = console.warn.bind(console); console.error = console.error.bind(console); window.addEventListener("load", function onLoad() { window.removeEventListener("load", onLoad);
--- a/browser/devtools/webide/modules/addons.js +++ b/browser/devtools/webide/modules/addons.js @@ -1,18 +1,20 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const {Cu} = require("chrome"); const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm"); const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm"); -const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); const {Services} = Cu.import("resource://gre/modules/Services.jsm"); -const {GetAddonsJSON} = require("devtools/webide/remote-resources"); +const {getJSON} = require("devtools/shared/getjson"); +const EventEmitter = require("devtools/toolkit/event-emitter"); + +const ADDONS_URL = "devtools.webide.addonsURL"; let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL"); let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL"); let ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL"); let SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID"); let ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID"); let ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID"); @@ -49,17 +51,17 @@ let GetAvailableAddons_promise = null; let GetAvailableAddons = exports.GetAvailableAddons = function() { if (!GetAvailableAddons_promise) { let deferred = promise.defer(); GetAvailableAddons_promise = deferred.promise; let addons = { simulators: [], adb: null } - GetAddonsJSON(true).then(json => { + getJSON(ADDONS_URL, true).then(json => { for (let stability in json) { for (let version of json[stability]) { addons.simulators.push(new SimulatorAddon(stability, version)); } } addons.adb = new ADBAddon(); addons.adapters = new AdaptersAddon(); deferred.resolve(addons);
--- a/browser/devtools/webide/moz.build +++ b/browser/devtools/webide/moz.build @@ -20,17 +20,16 @@ MOCHITEST_CHROME_MANIFESTS += [ ] EXTRA_JS_MODULES.devtools.webide += [ 'modules/addons.js', 'modules/app-manager.js', 'modules/build.js', 'modules/config-view.js', 'modules/project-list.js', - 'modules/remote-resources.js', 'modules/runtimes.js', 'modules/simulator-process.js', 'modules/simulators.js', 'modules/tab-store.js', 'modules/utils.js' ] JS_PREFERENCE_FILES += [
--- a/browser/locales/en-US/chrome/browser/devtools/device.properties +++ b/browser/locales/en-US/chrome/browser/devtools/device.properties @@ -9,11 +9,12 @@ # language in which you'd find the best documentation on web development on the # web. # LOCALIZATION NOTE: # These strings are category names in a list of devices that a user can choose # to simulate (e.g. "ZTE Open C", "VIA Vixen", "720p HD Television", etc). device.phones=Phones device.tablets=Tablets -device.notebooks=Notebooks +device.laptops=Laptops device.televisions=TVs +device.consoles=Gaming consoles device.watches=Watches
--- a/browser/metro/profile/metro.js +++ b/browser/metro/profile/metro.js @@ -539,17 +539,17 @@ pref("editor.singleLine.pasteNewlines", #ifdef MOZ_SERVICES_SYNC // sync service pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs"); // prefs to sync by default pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true); pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true); -pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true); +pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true); pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true); pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true); pref("services.sync.prefs.sync.privacy.donottrackheader.value", true); pref("services.sync.prefs.sync.signon.rememberSignons", true); #endif // threshold where a tap becomes a drag, in 1/240" reference pixels // The names of the preferences are to be in sync with EventStateManager.cpp
--- a/browser/modules/ReaderParent.jsm +++ b/browser/modules/ReaderParent.jsm @@ -8,16 +8,17 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components; this.EXPORTED_SYMBOLS = [ "ReaderParent" ]; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm"); const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties"); let ReaderParent = { MESSAGES: [ @@ -51,17 +52,28 @@ let ReaderParent = { // Make sure the target browser is still alive before trying to send data back. if (message.target.messageManager) { message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article }); } }); break; case "Reader:FaviconRequest": { - // XXX: To implement. + if (message.target.messageManager) { + let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url); + faviconUrl.then(function onResolution(favicon) { + message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", { + url: message.data.url, + faviconUrl: favicon.path.replace(/^favicon:/, "") + }) + }, + function onRejection(reason) { + Cu.reportError("Error requesting favicon URL for about:reader content: " + reason); + }).catch(Cu.reportError); + } break; } case "Reader:ListStatusRequest": ReadingList.hasItemForURL(message.data.url).then(inList => { let mm = message.target.messageManager // Make sure the target browser is still alive before trying to send data back. if (mm) { mm.sendAsyncMessage("Reader:ListStatusData",
--- a/browser/themes/shared/readinglist/sidebar.inc.css +++ b/browser/themes/shared/readinglist/sidebar.inc.css @@ -45,23 +45,28 @@ body { .item-thumb-container { min-width: 64px; max-width: 64px; min-height: 40px; max-height: 40px; border: 1px solid white; box-shadow: 0px 1px 2px rgba(0,0,0,.35); margin: 5px; - background-color: #fff; - background-size: cover; + background-color: #ebebeb; + background-size: contain; background-repeat: no-repeat; background-position: center; background-image: url("chrome://branding/content/silhouette-40.svg"); } +.item-thumb-container.preview-available { + background-color: #fff; + background-size: cover; +} + .item-summary-container { display: flex; flex-flow: column; -moz-padding-start: 4px; overflow: hidden; flex-grow: 1; } @@ -84,17 +89,17 @@ body { color: #0095DD; } .item:hover .item-domain { color: #008ACB; } .item:not(:hover):not(.selected) .remove-button { - display: none; + visibility: hidden; } .remove-button { padding: 0; width: 16px; height: 16px; background-size: contain; background-color: transparent;
--- a/mobile/android/base/TextSelection.java +++ b/mobile/android/base/TextSelection.java @@ -38,17 +38,17 @@ class TextSelection extends Layer implem private final TextSelectionHandle anchorHandle; private final TextSelectionHandle caretHandle; private final TextSelectionHandle focusHandle; private final DrawListener mDrawListener; private boolean mDraggingHandles; - private int selectionID; // Unique ID provided for each selection action. + private String selectionID; // Unique ID provided for each selection action. private float mViewLeft; private float mViewTop; private float mViewZoom; private String mCurrentItems; private TextSelectionActionModeCallback mCallback; @@ -127,17 +127,17 @@ class TextSelection extends Layer implem return; } ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { try { if (event.equals("TextSelection:ShowHandles")) { - selectionID = message.getInt("selectionID"); + selectionID = message.getString("selectionID"); final JSONArray handles = message.getJSONArray("handles"); for (int i=0; i < handles.length(); i++) { String handle = handles.getString(i); getHandle(handle).setVisibility(View.VISIBLE); } mViewLeft = 0.0f; mViewTop = 0.0f;
--- a/mobile/android/chrome/content/InputWidgetHelper.js +++ b/mobile/android/chrome/content/InputWidgetHelper.js @@ -8,17 +8,17 @@ var InputWidgetHelper = { handleEvent: function(aEvent) { this.handleClick(aEvent.target); }, handleClick: function(aTarget) { // if we're busy looking at a InputWidget we want to eat any clicks that // come to us, but not to process them - if (this._uiBusy || !this.hasInputWidget(aTarget)) + if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget)) return; this._uiBusy = true; this.show(aTarget); this._uiBusy = false; }, show: function(aElement) { @@ -76,10 +76,21 @@ var InputWidgetHelper = { fireOnChange: function(aElement) { let evt = aElement.ownerDocument.createEvent("Events"); evt.initEvent("change", true, true, aElement.defaultView, 0, false, false, false, false, null); setTimeout(function() { aElement.dispatchEvent(evt); }, 0); + }, + + _isDisabledElement : function(aElement) { + let currentElement = aElement; + while (currentElement) { + if (currentElement.disabled) + return true; + + currentElement = currentElement.parentElement; + } + return false; } };
--- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -40,17 +40,17 @@ var SelectionHandler = { // stored here are relative to the _contentWindow window. _cache: { anchorPt: {}, focusPt: {} }, _targetIsRTL: false, _anchorIsRTL: false, _focusIsRTL: false, _activeType: 0, // TYPE_NONE _selectionPrivate: null, // private selection reference - _selectionID: 0, // Unique Selection ID + _selectionID: null, // Unique Selection ID _draggingHandles: false, // True while user drags text selection handles _dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events _dragStartFocusOffset: null, // Editables need initial pos during HandleMove events _ignoreCompositionChanges: false, // Persist caret during IME composition updates _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages _deferCloseTimer: null, // Used to defer _closeSelection() actions during programmatic changes @@ -79,16 +79,23 @@ var SelectionHandler = { this._targetElementRef = Cu.getWeakReference(aTargetElement); }, get _domWinUtils() { return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIDOMWindowUtils); }, + // Provides UUID service for selection ID's. + get _idService() { + delete this._idService; + return this._idService = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + }, + _addObservers: function sh_addObservers() { Services.obs.addObserver(this, "Gesture:SingleTap", false); Services.obs.addObserver(this, "Tab:Selected", false); Services.obs.addObserver(this, "TextSelection:Move", false); Services.obs.addObserver(this, "TextSelection:Position", false); Services.obs.addObserver(this, "TextSelection:End", false); Services.obs.addObserver(this, "TextSelection:Action", false); Services.obs.addObserver(this, "TextSelection:LayerReflow", false); @@ -823,17 +830,17 @@ var SelectionHandler = { // Blur the targetElement to force IME code to undo previous style compositions // (visible underlines / etc generated by autoCorrection, autoSuggestion) aElement.blur(); } // Ensure targetElement is now focused normally aElement.focus(); } - this._selectionID++; + this._selectionID = this._idService.generateUUID().toString(); this._stopDraggingHandles(); this._contentWindow = aElement.ownerDocument.defaultView; this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl"); this._addObservers(); }, _getSelection: function sh_getSelection() {
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -7743,17 +7743,17 @@ var Distribution = { case "undefined": defaults.setCharPref(key, value); break; } } catch (e) { /* ignore bad prefs and move on */ } } // Apply a lightweight theme if necessary - if (prefs && prefs["lightweightThemes.isThemeSelected"]) { + if (prefs && prefs["lightweightThemes.selectedThemeID"]) { Services.obs.notifyObservers(null, "lightweight-theme-apply", ""); } let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); let localizeablePrefs = aData["LocalizablePreferences"]; for (let key in localizeablePrefs) { try { let value = localizeablePrefs[key];
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -832,16 +832,19 @@ pref("devtools.discovery.log", false); pref("devtools.remote.wifi.scan", false); // Hide UI options for controlling device visibility over WiFi // N.B.: This does not set whether the device can be discovered via WiFi, only // whether the UI control to make such a choice is shown to the user pref("devtools.remote.wifi.visible", false); // Client must complete TLS handshake within this window (ms) pref("devtools.remote.tls-handshake-timeout", 10000); +// URL of the remote JSON catalog used for device simulation +pref("devtools.devices.url", "https://code.cdn.mozilla.net/devices/devices.json"); + // view source pref("view_source.syntax_highlight", true); pref("view_source.wrap_long_lines", false); pref("view_source.editor.external", false); pref("view_source.editor.path", ""); // allows to add further arguments to the editor; use the %LINE% placeholder // for jumping to a specific line (e.g. "/line:%LINE%" or "--goto %LINE%") pref("view_source.editor.args", ""); @@ -4524,17 +4527,17 @@ pref("dom.mozSettings.SettingsService.ve pref("dom.mozSettings.allowForceReadOnly", false); // The interval at which to check for slow running addons #ifdef NIGHTLY_BUILD pref("browser.addon-watch.interval", 15000); #else pref("browser.addon-watch.interval", -1); #endif -pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\"]"); +pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\",\"fxdevtools-adapters@mozilla.org\",\"fx-devtools\"]"); // the percentage of time addons are allowed to use without being labeled slow pref("browser.addon-watch.percentage-limit", 5); // RequestSync API is disabled by default. pref("dom.requestSync.enabled", false); // Search service settings pref("browser.search.log", false);
--- a/services/sync/modules/engines/prefs.js +++ b/services/sync/modules/engines/prefs.js @@ -105,19 +105,18 @@ PrefStore.prototype = { // Missing prefs get the null value. values[pref] = this._prefs.get(pref, null); } } return values; }, _setAllPrefs: function PrefStore__setAllPrefs(values) { - let enabledPref = "lightweightThemes.isThemeSelected"; - let enabledBefore = this._prefs.get(enabledPref, false); - let prevTheme = LightweightThemeManager.currentTheme; + let selectedThemeIDPref = "lightweightThemes.selectedThemeID"; + let selectedThemeIDBefore = this._prefs.get(selectedThemeIDPref, null); for (let [pref, value] in Iterator(values)) { if (!this._isSynced(pref)) continue; // Pref has gone missing, best we can do is reset it. if (value == null) { this._prefs.reset(pref); @@ -126,23 +125,24 @@ PrefStore.prototype = { try { this._prefs.set(pref, value); } catch(ex) { this._log.trace("Failed to set pref: " + pref + ": " + ex); } } - // Notify the lightweight theme manager of all the new values - let enabledNow = this._prefs.get(enabledPref, false); - if (enabledBefore && !enabledNow) { + // Notify the lightweight theme manager if the selected theme has changed. + let selectedThemeIDAfter = this._prefs.get(selectedThemeIDPref, null); + if (selectedThemeIDBefore != selectedThemeIDAfter) { + // The currentTheme getter will reflect the theme with the new + // selectedThemeID (if there is one). Just reset it to itself + let currentTheme = LightweightThemeManager.currentTheme; LightweightThemeManager.currentTheme = null; - } else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) { - LightweightThemeManager.currentTheme = null; - LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0]; + LightweightThemeManager.currentTheme = currentTheme; } }, getAllIDs: function PrefStore_getAllIDs() { /* We store all prefs in just one WBO, with just one GUID */ let allprefs = {}; allprefs[PREFS_GUID] = true; return allprefs;
--- a/services/sync/tests/unit/test_prefs_store.js +++ b/services/sync/tests/unit/test_prefs_store.js @@ -90,38 +90,38 @@ function run_test() { do_check_eq(prefs.get("testing.deleteme"), undefined); do_check_eq(prefs.get("testing.dont.change"), "Please don't change me."); do_check_eq(Svc.Prefs.get("prefs.sync.testing.somepref"), true); _("Enable persona"); // Ensure we don't go to the network to fetch personas and end up leaking // stuff. Services.io.offline = true; - do_check_false(!!prefs.get("lightweightThemes.isThemeSelected")); + do_check_false(!!prefs.get("lightweightThemes.selectedThemeID")); do_check_eq(LightweightThemeManager.currentTheme, null); let persona1 = makePersona(); let persona2 = makePersona(); let usedThemes = JSON.stringify([persona1, persona2]); record.value = { - "lightweightThemes.isThemeSelected": true, + "lightweightThemes.selectedThemeID": persona1.id, "lightweightThemes.usedThemes": usedThemes }; store.update(record); - do_check_true(prefs.get("lightweightThemes.isThemeSelected")); + do_check_eq(prefs.get("lightweightThemes.selectedThemeID"), persona1.id); do_check_true(Utils.deepEquals(LightweightThemeManager.currentTheme, persona1)); _("Disable persona"); record.value = { - "lightweightThemes.isThemeSelected": false, + "lightweightThemes.selectedThemeID": null, "lightweightThemes.usedThemes": usedThemes }; store.update(record); - do_check_false(prefs.get("lightweightThemes.isThemeSelected")); + do_check_false(!!prefs.get("lightweightThemes.selectedThemeID")); do_check_eq(LightweightThemeManager.currentTheme, null); _("Only the current app's preferences are applied."); record = new PrefRec("prefs", "some-fake-app"); record.value = { "testing.int": 98 }; store.update(record);
--- a/storage/src/mozStorageAsyncStatementParams.h +++ b/storage/src/mozStorageAsyncStatementParams.h @@ -11,16 +11,18 @@ #include "nsIXPCScriptable.h" #include "mozilla/Attributes.h" class mozIStorageAsyncStatement; namespace mozilla { namespace storage { +class AsyncStatement; + /* * Since mozIStorageStatementParams is just a tagging interface we do not have * an async variant. */ class AsyncStatementParams final : public mozIStorageStatementParams , public nsIXPCScriptable { public:
--- a/storage/src/mozStorageStatementJSHelper.h +++ b/storage/src/mozStorageStatementJSHelper.h @@ -3,16 +3,17 @@ * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef MOZSTORAGESTATEMENTJSHELPER_H #define MOZSTORAGESTATEMENTJSHELPER_H #include "nsIXPCScriptable.h" +#include "nsIXPConnect.h" class Statement; namespace mozilla { namespace storage { class StatementJSHelper : public nsIXPCScriptable {
--- a/storage/src/mozStorageStatementRow.h +++ b/storage/src/mozStorageStatementRow.h @@ -9,16 +9,18 @@ #include "mozIStorageStatementRow.h" #include "nsIXPCScriptable.h" #include "mozilla/Attributes.h" namespace mozilla { namespace storage { +class Statement; + class StatementRow final : public mozIStorageStatementRow , public nsIXPCScriptable { public: NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGESTATEMENTROW NS_DECL_NSIXPCSCRIPTABLE
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp +++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp @@ -727,35 +727,70 @@ nsAutoCompleteController::SetSearchStrin NS_IMETHODIMP nsAutoCompleteController::GetSearchString(nsAString &aSearchString) { aSearchString = mSearchString; return NS_OK; } +void +nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch, + nsIAutoCompleteResult *aResult) +{ + // Look up the index of the search which is returning. + for (uint32_t i = 0; i < mSearches.Length(); ++i) { + if (mSearches[i] == aSearch) { + ProcessResult(i, aResult); + } + } +} + //////////////////////////////////////////////////////////////////////// //// nsIAutoCompleteObserver NS_IMETHODIMP nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) { + MOZ_ASSERT(mSearches.Contains(aSearch)); + ClearResults(); - return OnSearchResult(aSearch, aResult); + HandleSearchResult(aSearch, aResult); + return NS_OK; } NS_IMETHODIMP nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult) { - // look up the index of the search which is returning - for (uint32_t i = 0; i < mSearches.Length(); ++i) { - if (mSearches[i] == aSearch) { - ProcessResult(i, aResult); - } + MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch)); + + // If this is the first search result we are processing + // we should clear out the previously cached results. + if (mFirstSearchResult) { + ClearResults(); + mFirstSearchResult = false; + } + + uint16_t result = 0; + if (aResult) { + aResult->GetSearchResult(&result); + } + + // If our results are incremental, the search is still ongoing. + if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && + result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { + --mSearchesOngoing; + } + + HandleSearchResult(aSearch, aResult); + + if (mSearchesOngoing == 0) { + // If this is the last search to return, cleanup. + PostSearchCleanup(); } return NS_OK; } //////////////////////////////////////////////////////////////////////// //// nsITimerCallback @@ -1070,18 +1105,23 @@ nsAutoCompleteController::BeforeSearches } nsresult nsAutoCompleteController::StartSearch(uint16_t aSearchType) { NS_ENSURE_STATE(mInput); nsCOMPtr<nsIAutoCompleteInput> input = mInput; - for (uint32_t i = 0; i < mSearches.Length(); ++i) { - nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i]; + // Iterate a copy of |mSearches| so that we don't run into trouble if the + // array is mutated while we're still in the loop. An nsIAutoCompleteSearch + // implementation could synchronously start a new search when StartSearch() + // is called and that would lead to assertions down the way. + nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches); + for (uint32_t i = 0; i < searchesCopy.Length(); ++i) { + nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i]; // Filter on search type. Not all the searches implement this interface, // in such a case just consider them delayed. uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED; nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc = do_QueryInterface(search); if (searchDesc) searchDesc->GetSearchType(&searchType); @@ -1102,16 +1142,17 @@ nsAutoCompleteController::StartSearch(ui nsAutoString searchParam; nsresult rv = input->GetSearchParam(searchParam); if (NS_FAILED(rv)) return rv; rv = search->StartSearch(mSearchString, searchParam, result, static_cast<nsIAutoCompleteObserver *>(this)); if (NS_FAILED(rv)) { ++mSearchesFailed; + MOZ_ASSERT(mSearchesOngoing > 0); --mSearchesOngoing; } // Because of the joy of nested event loops (which can easily happen when some // code uses a generator for an asynchronous AutoComplete search), // nsIAutoCompleteSearch::StartSearch might cause us to be detached from our input // field. The next time we iterate, we'd be touching something that we shouldn't // be, and result in a crash. if (!mInput) { @@ -1424,33 +1465,20 @@ nsAutoCompleteController::RevertTextValu } nsresult nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult) { NS_ENSURE_STATE(mInput); nsCOMPtr<nsIAutoCompleteInput> input(mInput); - // If this is the first search result we are processing - // we should clear out the previously cached results - if (mFirstSearchResult) { - ClearResults(); - mFirstSearchResult = false; - } - uint16_t result = 0; if (aResult) aResult->GetSearchResult(&result); - // if our results are incremental, the search is still ongoing - if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING && - result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { - --mSearchesOngoing; - } - uint32_t oldMatchCount = 0; uint32_t matchCount = 0; if (aResult) aResult->GetMatchCount(&matchCount); int32_t resultIndex = mResults.IndexOf(aResult); if (resultIndex == -1) { // cache the result @@ -1500,32 +1528,27 @@ nsAutoCompleteController::ProcessResult( uint32_t minResults; input->GetMinResultsForPopup(&minResults); // Make sure the popup is open, if necessary, since we now have at least one // search result ready to display. Don't force the popup closed if we might // get results in the future to avoid unnecessarily canceling searches. if (mRowCount || !minResults) { OpenPopup(); - } else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) { + } else if (mSearchesOngoing == 0) { ClosePopup(); } } if (result == nsIAutoCompleteResult::RESULT_SUCCESS || result == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) { // Try to autocomplete the default index for this search. CompleteDefaultIndex(resultIndex); } - if (mSearchesOngoing == 0) { - // If this is the last search to return, cleanup. - PostSearchCleanup(); - } - return NS_OK; } nsresult nsAutoCompleteController::PostSearchCleanup() { NS_ENSURE_STATE(mInput); nsCOMPtr<nsIAutoCompleteInput> input(mInput);
--- a/toolkit/components/autocomplete/nsAutoCompleteController.h +++ b/toolkit/components/autocomplete/nsAutoCompleteController.h @@ -45,16 +45,18 @@ protected: nsresult StartSearch(uint16_t aSearchType); nsresult BeforeSearches(); nsresult StartSearches(); void AfterSearches(); nsresult ClearSearchTimer(); void MaybeCompletePlaceholder(); + void HandleSearchResult(nsIAutoCompleteSearch *aSearch, + nsIAutoCompleteResult *aResult); nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult); nsresult PostSearchCleanup(); nsresult EnterMatch(bool aIsPopupSelection); nsresult RevertTextValue(); nsresult CompleteDefaultIndex(int32_t aResultIndex); nsresult CompleteValue(nsString &aValue);
--- a/toolkit/components/passwordmgr/moz.build +++ b/toolkit/components/passwordmgr/moz.build @@ -38,17 +38,16 @@ EXTRA_PP_COMPONENTS += [ EXTRA_PP_JS_MODULES += [ 'LoginManagerParent.jsm', ] EXTRA_JS_MODULES += [ 'InsecurePasswordUtils.jsm', 'LoginHelper.jsm', 'LoginManagerContent.jsm', - 'LoginManagerParent.jsm', 'LoginRecipes.jsm', ] if CONFIG['OS_TARGET'] == 'Android': EXTRA_COMPONENTS += [ 'storage-mozStorage.js', ] else:
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js +++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js @@ -9,22 +9,20 @@ Cu.import("resource://gre/modules/XPCOMU Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); Cu.import("resource://gre/modules/SharedPromptUtils.jsm"); /* Constants for password prompt telemetry. * Mirrored in mobile/android/components/LoginManagerPrompter.js */ const PROMPT_DISPLAYED = 0; -const PROMPT_ADD = 1; +const PROMPT_ADD_OR_UPDATE = 1; const PROMPT_NOTNOW = 2; const PROMPT_NEVER = 3; -const PROMPT_UPDATE = 1; - /* * LoginManagerPromptFactory * * Implements nsIPromptFactory * * Invoked by [toolkit/components/prompts/src/nsPrompter.js] */ function LoginManagerPromptFactory() { @@ -737,18 +735,16 @@ LoginManagerPrompter.prototype = { /* * promptToSavePassword * */ promptToSavePassword : function (aLogin) { var notifyObj = this._getPopupNote() || this._getNotifyBox(); - Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED); - if (notifyObj) this._showSaveLoginNotification(notifyObj, aLogin); else this._showSaveLoginDialog(aLogin); }, /* @@ -778,16 +774,110 @@ LoginManagerPrompter.prototype = { newBar.timeout = Date.now() + 20000; // 20 seconds if (oldBar) { this.log("(...and removing old " + aName + " notification bar)"); aNotifyBox.removeNotification(oldBar); } }, + /** + * Displays the PopupNotifications.jsm doorhanger for password save or change. + * + * @param {nsILoginInfo} login + * Login to save or change. For changes, this login should contain the + * new password. + * @param {string} type + * This is "password-save" or "password-change" depending on the + * original notification type. This is used for telemetry and tests. + */ + _showLoginCaptureDoorhanger(login, type) { + let { browser } = this._getNotifyWindow(); + + let msgNames = type == "password-save" ? { + prompt: "rememberPasswordMsgNoUsername", + buttonLabel: "notifyBarRememberPasswordButtonText", + buttonAccessKey: "notifyBarRememberPasswordButtonAccessKey", + } : { + // We reuse the existing message, even if it expects a username, until we + // switch to the final terminology in bug 1144856. + prompt: "updatePasswordMsg", + buttonLabel: "notifyBarUpdateButtonText", + buttonAccessKey: "notifyBarUpdateButtonAccessKey", + }; + + let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION" + : "PWMGR_PROMPT_UPDATE_ACTION"; + let histogram = Services.telemetry.getHistogramById(histogramName); + histogram.add(PROMPT_DISPLAYED); + + // The main action is the "Remember" or "Update" button. + let mainAction = { + label: this._getLocalizedString(msgNames.buttonLabel), + accessKey: this._getLocalizedString(msgNames.buttonAccessKey), + callback: () => { + histogram.add(PROMPT_ADD_OR_UPDATE); + let foundLogins = Services.logins.findLogins({}, login.hostname, + login.formSubmitURL, + login.httpRealm); + let logins = foundLogins.filter(l => l.username == login.username); + if (logins.length == 0) { + Services.logins.addLogin(login); + } else if (logins.length == 1) { + this._updateLogin(logins[0], login.password); + } else { + Cu.reportError("Unexpected match of multiple logins."); + } + browser.focus(); + } + }; + + // Include a "Never for this site" button when saving a new password. + let secondaryActions = type == "password-save" ? [{ + label: this._getLocalizedString("notifyBarNeverRememberButtonText"), + accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"), + callback: () => { + histogram.add(PROMPT_NEVER); + Services.logins.setLoginSavingEnabled(login.hostname, false); + browser.focus(); + } + }] : null; + + let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder"); + let displayHost = this._getShortDisplayHost(login.hostname); + + this._getPopupNote().show( + browser, + "password", + this._getLocalizedString(msgNames.prompt, [displayHost]), + "password-notification-icon", + mainAction, + secondaryActions, + { + timeout: Date.now() + 10000, + persistWhileVisible: true, + passwordNotificationType: type, + eventCallback: function (topic) { + if (topic != "showing") { + return false; + } + + let chromeDoc = this.browser.ownerDocument; + + chromeDoc.getElementById("password-notification-username") + .setAttribute("placeholder", usernamePlaceholder); + chromeDoc.getElementById("password-notification-username") + .setAttribute("value", login.username); + chromeDoc.getElementById("password-notification-password") + .setAttribute("value", login.password); + }, + } + ); + }, + /* * _showSaveLoginNotification * * Displays a notification bar or a popup notification, to allow the user * to save the specified login. This allows the user to see the results of * their login, and only save a login which they know worked. * * @param aNotifyObj @@ -801,80 +891,30 @@ LoginManagerPrompter.prototype = { var neverButtonText = this._getLocalizedString("notifyBarNeverRememberButtonText"); var neverButtonAccessKey = this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"); var rememberButtonText = this._getLocalizedString("notifyBarRememberPasswordButtonText"); var rememberButtonAccessKey = this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); - var usernamePlaceholder = - this._getLocalizedString("noUsernamePlaceholder"); var displayHost = this._getShortDisplayHost(aLogin.hostname); var notificationText = this._getLocalizedString( "rememberPasswordMsgNoUsername", [displayHost]); // The callbacks in |buttons| have a closure to access the variables // in scope here; set one to |this._pwmgr| so we can get back to pwmgr // without a getService() call. var pwmgr = this._pwmgr; - let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION"); // Notification is a PopupNotification if (aNotifyObj == this._getPopupNote()) { - // "Remember" button - var mainAction = { - label: rememberButtonText, - accessKey: rememberButtonAccessKey, - callback: function(aNotifyObj, aButton) { - promptHistogram.add(PROMPT_ADD); - pwmgr.addLogin(aLogin); - browser.focus(); - } - }; - - var secondaryActions = [ - // "Never for this site" button - { - label: neverButtonText, - accessKey: neverButtonAccessKey, - callback: function(aNotifyObj, aButton) { - promptHistogram.add(PROMPT_NEVER); - pwmgr.setLoginSavingEnabled(aLogin.hostname, false); - browser.focus(); - } - } - ]; - - var { browser } = this._getNotifyWindow(); - - let eventCallback = function (topic) { - if (topic != "showing") { - return false; - } - - let chromeDoc = this.browser.ownerDocument; - - chromeDoc.getElementById("password-notification-username") - .setAttribute("placeholder", usernamePlaceholder); - chromeDoc.getElementById("password-notification-username") - .setAttribute("value", aLogin.username); - chromeDoc.getElementById("password-notification-password") - .setAttribute("value", aLogin.password); - }; - - aNotifyObj.show(browser, "password", notificationText, - "password-notification-icon", mainAction, - secondaryActions, - { timeout: Date.now() + 10000, - persistWhileVisible: true, - passwordNotificationType: "password-save", - eventCallback }); + this._showLoginCaptureDoorhanger(aLogin, "password-save"); } else { var notNowButtonText = this._getLocalizedString("notifyBarNotNowButtonText"); var notNowButtonAccessKey = this._getLocalizedString("notifyBarNotNowButtonAccessKey"); var buttons = [ // "Remember" button { @@ -1025,68 +1065,32 @@ LoginManagerPrompter.prototype = { * @param aNotifyObj * A notification box or a popup notification. */ _showChangeLoginNotification : function (aNotifyObj, aOldLogin, aNewPassword) { var changeButtonText = this._getLocalizedString("notifyBarUpdateButtonText"); var changeButtonAccessKey = this._getLocalizedString("notifyBarUpdateButtonAccessKey"); - var usernamePlaceholder = - this._getLocalizedString("noUsernamePlaceholder"); // We reuse the existing message, even if it expects a username, until we // switch to the final terminology in bug 1144856. var displayHost = this._getShortDisplayHost(aOldLogin.hostname); var notificationText = this._getLocalizedString("updatePasswordMsg", [displayHost]); // The callbacks in |buttons| have a closure to access the variables // in scope here; set one to |this._pwmgr| so we can get back to pwmgr // without a getService() call. var self = this; - let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION"); // Notification is a PopupNotification if (aNotifyObj == this._getPopupNote()) { - // "Yes" button - var mainAction = { - label: changeButtonText, - accessKey: changeButtonAccessKey, - popup: null, - callback: function(aNotifyObj, aButton) { - self._updateLogin(aOldLogin, aNewPassword); - promptHistogram.add(PROMPT_UPDATE); - } - }; - - var { browser } = this._getNotifyWindow(); - - let eventCallback = function (topic) { - if (topic != "showing") { - return false; - } - - let chromeDoc = this.browser.ownerDocument; - - chromeDoc.getElementById("password-notification-username") - .setAttribute("placeholder", usernamePlaceholder); - chromeDoc.getElementById("password-notification-username") - .setAttribute("value", aOldLogin.username); - chromeDoc.getElementById("password-notification-password") - .setAttribute("value", aNewPassword); - }; - - Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED); - aNotifyObj.show(browser, "password", notificationText, - "password-notification-icon", mainAction, - null, { timeout: Date.now() + 10000, - persistWhileVisible: true, - passwordNotificationType: "password-change", - eventCallback }); + aOldLogin.password = aNewPassword; + this._showLoginCaptureDoorhanger(aOldLogin, "password-change"); } else { var dontChangeButtonText = this._getLocalizedString("notifyBarDontChangeButtonText"); var dontChangeButtonAccessKey = this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); var buttons = [ // "Yes" button {
--- a/toolkit/components/places/PlacesUtils.jsm +++ b/toolkit/components/places/PlacesUtils.jsm @@ -1557,17 +1557,17 @@ this.PlacesUtils = { if (!(aPageUrl instanceof Ci.nsIURI)) aPageUrl = NetUtil.newURI(aPageUrl); PlacesUtils.favicons.getFaviconURLForPage(aPageUrl, uri => { if (uri) { uri = PlacesUtils.favicons.getFaviconLinkForIcon(uri); deferred.resolve(uri); } else { - deferred.reject(); + deferred.reject("favicon not found for uri"); } }); return deferred.promise; }, /** * Returns the passed URL with a #-moz-resolution fragment * for the specified dimensions and devicePixelRatio.
--- a/toolkit/components/reader/Readability.js +++ b/toolkit/components/reader/Readability.js @@ -98,17 +98,17 @@ Readability.prototype = { unlikelyCandidates: /combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|sponsor|ad-break|agegate|pagination|pager|popup|tweet|twitter/i, okMaybeItsACandidate: /and|article|body|column|main|shadow/i, positive: /article|body|content|entry|hentry|main|page|pagination|post|text|blog|story/i, negative: /hidden|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|sponsor|shopping|tags|tool|widget/i, extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i, byline: /byline|author|dateline|writtenby/i, replaceFonts: /<(\/?)font[^>]*>/gi, normalize: /\s{2,}/g, - videos: /https?:\/\/(www\.)?(youtube|vimeo)\.com/i, + videos: /https?:\/\/(www\.)?(youtube|youtube-nocookie|player\.vimeo)\.com/i, nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i, prevLink: /(prev|earl|old|new|<|«)/i, whitespace: /^\s*$/, hasContent: /\S$/, }, DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ], @@ -121,16 +121,46 @@ Readability.prototype = { * @return void **/ _postProcessContent: function(articleContent) { // Readability cannot open relative uris so we convert them to absolute uris. this._fixRelativeUris(articleContent); }, /** + * Iterate over a NodeList, which doesn't natively fully implement the Array + * interface. + * + * For convenience, the current object context is applied to the provided + * iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return void + */ + _forEachNode: function(nodeList, fn) { + return Array.prototype.forEach.call(nodeList, fn, this); + }, + + /** + * Iterate over a NodeList, return true if any of the provided iterate + * function calls returns true, false otherwise. + * + * For convenience, the current object context is applied to the + * provided iterate function. + * + * @param NodeList nodeList The NodeList. + * @param Function fn The iterate function. + * @return Boolean + */ + _someNode: function(nodeList, fn) { + return Array.prototype.some.call(nodeList, fn, this); + }, + + /** * Converts each <a> and <img> uri in the given element to an absolute URI. * * @param Element * @return void */ _fixRelativeUris: function(articleContent) { var scheme = this._uri.scheme; var prePath = this._uri.prePath; @@ -144,36 +174,39 @@ Readability.prototype = { // Scheme-rooted relative URI. if (uri.substr(0, 2) == "//") return scheme + "://" + uri.substr(2); // Prepath-rooted relative URI. if (uri[0] == "/") return prePath + uri; + // Dotslash relative URI. + if (uri.indexOf("./") === 0) + return pathBase + uri.slice(2); + // Standard relative URI; add entire path. pathBase already includes a // trailing "/". return pathBase + uri; } function convertRelativeURIs(tagName, propName) { var elems = articleContent.getElementsByTagName(tagName); - for (var i = elems.length; --i >= 0;) { - var elem = elems[i]; + this._forEachNode(elems, function(elem) { var relativeURI = elem.getAttribute(propName); if (relativeURI != null) - elems[i].setAttribute(propName, toAbsoluteURI(relativeURI)); - } + elem.setAttribute(propName, toAbsoluteURI(relativeURI)); + }); } // Fix links. - convertRelativeURIs("a", "href"); + convertRelativeURIs.call(this, "a", "href"); // Fix images. - convertRelativeURIs("img", "src"); + convertRelativeURIs.call(this, "img", "src"); }, /** * Get the article title as an H1. * * @return void **/ _getArticleTitle: function() { @@ -219,29 +252,27 @@ Readability.prototype = { * This includes things like stripping javascript, CSS, and handling terrible markup. * * @return void **/ _prepDocument: function() { var doc = this._doc; // Remove all style tags in head - var styleTags = doc.getElementsByTagName("style"); - for (var st = styleTags.length - 1; st >= 0; st -= 1) { - styleTags[st].parentNode.removeChild(styleTags[st]); - } + this._forEachNode(doc.getElementsByTagName("style"), function(styleNode) { + styleNode.parentNode.removeChild(styleNode); + }); if (doc.body) { this._replaceBrs(doc.body); } - var fonts = doc.getElementsByTagName("FONT"); - for (var i = fonts.length; --i >=0;) { - this._setNodeTag(fonts[i], "SPAN"); - } + this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) { + this._setNodeTag(fontNode, "SPAN"); + }); }, /** * Finds the next element, starting from the given node, and ignoring * whitespace in between. If the given node is an element, the same node is * returned. */ _nextElement: function (node) { @@ -257,19 +288,17 @@ Readability.prototype = { /** * Replaces 2 or more successive <br> elements with a single <p>. * Whitespace between <br> elements are ignored. For example: * <div>foo<br>bar<br> <br><br>abc</div> * will become: * <div>foo<br>bar<p>abc</p></div> */ _replaceBrs: function (elem) { - var brs = elem.getElementsByTagName("br"); - for (var i = 0; i < brs.length; i++) { - var br = brs[i]; + this._forEachNode(elem.getElementsByTagName("br"), function(br) { var next = br.nextSibling; // Whether 2 or more <br> elements have been found and replaced with a // <p> block. var replaced = false; // If we find a <br> chain, remove the <br>s until we hit another element // or non-whitespace. This leaves behind the first <br> in the chain @@ -298,17 +327,17 @@ Readability.prototype = { } // Otherwise, make this node a child of the new <p>. var sibling = next.nextSibling; p.appendChild(next); next = sibling; } } - } + }); }, _setNodeTag: function (node, tag) { // FIXME this doesn't work on anything but JSDOMParser (ie the node's tag // won't actually be set). node.localName = tag.toLowerCase(); node.tagName = tag.toUpperCase(); }, @@ -321,16 +350,17 @@ Readability.prototype = { * @return void **/ _prepArticle: function(articleContent) { this._cleanStyles(articleContent); // Clean out junk from the article content this._cleanConditionally(articleContent, "form"); this._clean(articleContent, "object"); + this._clean(articleContent, "embed"); this._clean(articleContent, "h1"); // If there is only one h2, they are probably using it as a header // and not a subheader, so remove it since we already have a header. if (articleContent.getElementsByTagName('h2').length === 1) this._clean(articleContent, "h2"); this._clean(articleContent, "iframe"); @@ -338,36 +368,33 @@ Readability.prototype = { // Do these last as the previous stuff may have removed junk // that will affect these this._cleanConditionally(articleContent, "table"); this._cleanConditionally(articleContent, "ul"); this._cleanConditionally(articleContent, "div"); // Remove extra paragraphs - var articleParagraphs = articleContent.getElementsByTagName('p'); - for (var i = articleParagraphs.length - 1; i >= 0; i -= 1) { - var imgCount = articleParagraphs[i].getElementsByTagName('img').length; - var embedCount = articleParagraphs[i].getElementsByTagName('embed').length; - var objectCount = articleParagraphs[i].getElementsByTagName('object').length; + this._forEachNode(articleContent.getElementsByTagName('p'), function(paragraph) { + var imgCount = paragraph.getElementsByTagName('img').length; + var embedCount = paragraph.getElementsByTagName('embed').length; + var objectCount = paragraph.getElementsByTagName('object').length; + // At this point, nasty iframes have been removed, only remain embedded video ones. + var iframeCount = paragraph.getElementsByTagName('iframe').length; + var totalCount = imgCount + embedCount + objectCount + iframeCount; - if (imgCount === 0 && - embedCount === 0 && - objectCount === 0 && - this._getInnerText(articleParagraphs[i], false) === '') - articleParagraphs[i].parentNode.removeChild(articleParagraphs[i]); - } + if (totalCount === 0 && !this._getInnerText(paragraph, false)) + paragraph.parentNode.removeChild(paragraph); + }); - var brs = articleContent.getElementsByTagName("BR"); - for (var i = brs.length; --i >= 0;) { - var br = brs[i]; + this._forEachNode(articleContent.getElementsByTagName("br"), function(br) { var next = this._nextElement(br.nextSibling); if (next && next.tagName == "P") br.parentNode.removeChild(br); - } + }); }, /** * Initialize a node with the readability object. Also checks the * className/id for special names to add to its score. * * @param Element * @return void @@ -524,49 +551,48 @@ Readability.prototype = { var newNode = node.firstElementChild; node.parentNode.replaceChild(newNode, node); node = newNode; } else if (!this._hasChildBlockElement(node)) { this._setNodeTag(node, "P"); elementsToScore.push(node); } else { // EXPERIMENTAL - for (var i = 0, il = node.childNodes.length; i < il; i += 1) { - var childNode = node.childNodes[i]; + this._forEachNode(node.childNodes, function(childNode) { if (childNode.nodeType === Node.TEXT_NODE) { var p = doc.createElement('p'); p.textContent = childNode.textContent; p.style.display = 'inline'; p.className = 'readability-styled'; node.replaceChild(p, childNode); } - } + }); } } node = this._getNextNode(node); } /** * Loop through all paragraphs, and assign a score to them based on how content-y they look. * Then add their score to their parent node. * * A score is determined by things like number of commas, class names, etc. Maybe eventually link density. **/ var candidates = []; - for (var pt = 0; pt < elementsToScore.length; pt += 1) { - var parentNode = elementsToScore[pt].parentNode; + this._forEachNode(elementsToScore, function(elementToScore) { + var parentNode = elementToScore.parentNode; var grandParentNode = parentNode ? parentNode.parentNode : null; - var innerText = this._getInnerText(elementsToScore[pt]); + var innerText = this._getInnerText(elementToScore); if (!parentNode || typeof(parentNode.tagName) === 'undefined') - continue; + return; // If this paragraph is less than 25 characters, don't even count it. if (innerText.length < 25) - continue; + return; // Initialize readability data for the parent. if (typeof parentNode.readability === 'undefined') { this._initializeNode(parentNode); candidates.push(parentNode); } // Initialize readability data for the grandparent. @@ -588,17 +614,17 @@ Readability.prototype = { // For every 100 characters in this paragraph, add another point. Up to 3 points. contentScore += Math.min(Math.floor(innerText.length / 100), 3); // Add the score to the parent. The grandparent gets half. parentNode.readability.contentScore += contentScore; if (grandParentNode) grandParentNode.readability.contentScore += contentScore / 2; - } + }); // After we've calculated scores, loop through all of the possible // candidate nodes we found and find the one with the highest score. var topCandidates = []; for (var c = 0, cl = candidates.length; c < cl; c += 1) { var candidate = candidates[c]; // Scale the final candidates score based on link density. Good content @@ -645,28 +671,29 @@ Readability.prototype = { // Because of our bonus system, parents of candidates might have scores // themselves. They get half of the node. There won't be nodes with higher // scores than our topCandidate, but if we see the score going *up* in the first // few steps up the tree, that's a decent sign that there might be more content // lurking in other places that we want to unify in. The sibling stuff // below does some of that - but only if we've looked high enough up the DOM // tree. var parentOfTopCandidate = topCandidate.parentNode; + var lastScore = topCandidate.readability.contentScore; // The scores shouldn't get too low. - var scoreThreshold = topCandidate.readability.contentScore / 3; - var lastScore = parentOfTopCandidate.readability.contentScore; + var scoreThreshold = lastScore / 3; while (parentOfTopCandidate && parentOfTopCandidate.readability) { var parentScore = parentOfTopCandidate.readability.contentScore; if (parentScore < scoreThreshold) break; if (parentScore > lastScore) { // Alright! We found a better parent to use. topCandidate = parentOfTopCandidate; break; } + lastScore = parentOfTopCandidate.readability.contentScore; parentOfTopCandidate = parentOfTopCandidate.parentNode; } } // Now that we have the top candidate, look through its siblings for content // that might also be related. Things like preambles, content split by ads // that we removed, etc. var articleContent = doc.createElement("DIV"); @@ -799,40 +826,39 @@ Readability.prototype = { byline = byline.trim(); return (byline.length > 0) && (byline.length < 100); } return false; }, /** * Attempts to get excerpt and byline metadata for the article. - * + * * @return Object with optional "excerpt" and "byline" properties */ _getArticleMetadata: function() { var metadata = {}; var values = {}; var metaElements = this._doc.getElementsByTagName("meta"); // Match "description", or Twitter's "twitter:description" (Cards) // in name attribute. var namePattern = /^\s*((twitter)\s*:\s*)?description\s*$/gi; // Match Facebook's og:description (Open Graph) in property attribute. var propertyPattern = /^\s*og\s*:\s*description\s*$/gi; // Find description tags. - for (var i = 0; i < metaElements.length; i++) { - var element = metaElements[i]; + this._forEachNode(metaElements, function(element) { var elementName = element.getAttribute("name"); var elementProperty = element.getAttribute("property"); if (elementName === "author") { metadata.byline = element.getAttribute("content"); - continue; + return; } var name = null; if (namePattern.test(elementName)) { name = elementName; } else if (propertyPattern.test(elementProperty)) { name = elementProperty; } @@ -841,17 +867,17 @@ Readability.prototype = { var content = element.getAttribute("content"); if (content) { // Convert to lowercase and remove any whitespace // so we can match below. name = name.toLowerCase().replace(/\s/g, ''); values[name] = content.trim(); } } - } + }); if ("description" in values) { metadata.excerpt = values["description"]; } else if ("og:description" in values) { // Use facebook open graph description. metadata.excerpt = values["og:description"]; } else if ("twitter:description" in values) { // Use twitter cards description. @@ -862,76 +888,68 @@ Readability.prototype = { }, /** * Removes script tags from the document. * * @param Element **/ _removeScripts: function(doc) { - var scripts = doc.getElementsByTagName('script'); - for (var i = scripts.length - 1; i >= 0; i -= 1) { - scripts[i].nodeValue=""; - scripts[i].removeAttribute('src'); + this._forEachNode(doc.getElementsByTagName('script'), function(scriptNode) { + scriptNode.nodeValue = ""; + scriptNode.removeAttribute('src'); - if (scripts[i].parentNode) - scripts[i].parentNode.removeChild(scripts[i]); - } + if (scriptNode.parentNode) + scriptNode.parentNode.removeChild(scriptNode); + }); }, /** * Check if this node has only whitespace and a single P element * Returns false if the DIV node contains non-empty text nodes * or if it contains no P or more than 1 element. * * @param Element **/ - _hasSinglePInsideElement: function(e) { + _hasSinglePInsideElement: function(element) { // There should be exactly 1 element child which is a P: - if (e.children.length != 1 || e.firstElementChild.tagName !== "P") { + if (element.children.length != 1 || element.firstElementChild.tagName !== "P") { return false; } + // And there should be no text nodes with real content - var childNodes = e.childNodes; - for (var i = childNodes.length; --i >= 0;) { - var node = childNodes[i]; - if (node.nodeType == Node.TEXT_NODE && - this.REGEXPS.hasContent.test(node.textContent)) { - return false; - } - } - - return true; + return !this._someNode(element.childNodes, function(node) { + return node.nodeType === Node.TEXT_NODE && + this.REGEXPS.hasContent.test(node.textContent); + }); }, /** * Determine whether element has any children block level elements. * * @param Element */ - _hasChildBlockElement: function (e) { - var length = e.children.length; - for (var i = 0; i < length; i++) { - var child = e.children[i]; - if (this.DIV_TO_P_ELEMS.indexOf(child.tagName) !== -1 || this._hasChildBlockElement(child)) - return true; - } - return false; + _hasChildBlockElement: function (element) { + return this._someNode(element.childNodes, function(node) { + return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 || + this._hasChildBlockElement(node); + }); }, /** * Get the inner text of a node - cross browser compatibly. * This also strips out any excess whitespace to be found. * * @param Element + * @param Boolean normalizeSpaces (default: true) * @return string **/ _getInnerText: function(e, normalizeSpaces) { + normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces; var textContent = e.textContent.trim(); - normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces; if (normalizeSpaces) { return textContent.replace(this.REGEXPS.normalize, " "); } else { return textContent; } }, @@ -980,24 +998,27 @@ Readability.prototype = { /** * Get the density of links as a percentage of the content * This is the amount of text that is inside a link divided by the total text in the node. * * @param Element * @return number (float) **/ - _getLinkDensity: function(e) { - var links = e.getElementsByTagName("a"); - var textLength = this._getInnerText(e).length; + _getLinkDensity: function(element) { + var textLength = this._getInnerText(element).length; + if (textLength === 0) + return; + var linkLength = 0; - for (var i = 0, il = links.length; i < il; i += 1) { - linkLength += this._getInnerText(links[i]).length; - } + // XXX implement _reduceNodeList? + this._forEachNode(element.getElementsByTagName("a"), function(linkNode) { + linkLength += this._getInnerText(linkNode).length; + }); return linkLength / textLength; }, /** * Find a cleaned up version of the current URL, to use for comparing links for possible next-pageyness. * * @author Dan Lacy @@ -1400,38 +1421,36 @@ Readability.prototype = { * Clean a node of all elements of type "tag". * (Unless it's a youtube/vimeo video. People love movies.) * * @param Element * @param string tag to clean * @return void **/ _clean: function(e, tag) { - var targetList = e.getElementsByTagName(tag); - var isEmbed = (tag === 'object' || tag === 'embed'); + var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1; - for (var y = targetList.length - 1; y >= 0; y -= 1) { + this._forEachNode(e.getElementsByTagName(tag), function(element) { // Allow youtube and vimeo videos through as people usually want to see those. if (isEmbed) { - var attributeValues = ""; - for (var i = 0, il = targetList[y].attributes.length; i < il; i += 1) { - attributeValues += targetList[y].attributes[i].value + '|'; - } + var attributeValues = [].map.call(element.attributes, function(attr) { + return attr.value; + }).join("|"); // First, check the elements attributes to see if any of them contain youtube or vimeo if (this.REGEXPS.videos.test(attributeValues)) - continue; + return; // Then check the elements inside this element for the same. - if (this.REGEXPS.videos.test(targetList[y].innerHTML)) - continue; + if (this.REGEXPS.videos.test(element.innerHTML)) + return; } - targetList[y].parentNode.removeChild(targetList[y]); - } + element.parentNode.removeChild(element); + }); }, /** * Clean an element of all tags of type "tag" if they look fishy. * "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc. * * @return void **/ @@ -1573,17 +1592,17 @@ Readability.prototype = { // } // If we haven't found an excerpt in the article's metadata, use the article's // first paragraph as the excerpt. This is used for displaying a preview of // the article's content. if (!metadata.excerpt) { var paragraphs = articleContent.getElementsByTagName("p"); if (paragraphs.length > 0) { - metadata.excerpt = paragraphs[0].textContent; + metadata.excerpt = paragraphs[0].textContent.trim(); } } return { uri: this._uri, title: articleTitle, byline: metadata.byline || this._articleByline, dir: this._articleDir, content: articleContent.innerHTML,
--- a/toolkit/components/satchel/nsFormAutoComplete.js +++ b/toolkit/components/satchel/nsFormAutoComplete.js @@ -269,24 +269,30 @@ FormAutoComplete.prototype = { let processResults = { handleResult: aResult => { results.push(aResult); }, handleError: aError => { this.log("getAutocompleteValues failed: " + aError.message); }, handleCompletion: aReason => { - this._pendingQuery = null; - if (!aReason) { - callback(results); + // Check that the current query is still the one we created. Our + // query might have been canceled shortly before completing, in + // that case we don't want to call the callback anymore. + if (query == this._pendingQuery) { + this._pendingQuery = null; + if (!aReason) { + callback(results); + } } } }; - this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults); + let query = FormHistory.getAutoCompleteResults(searchString, params, processResults); + this._pendingQuery = query; }, /* * _calculateScore * * entry -- an nsIAutoCompleteResult entry * aSearchString -- current value of the input (lowercase) * searchTokens -- array of tokens of the search string @@ -324,16 +330,17 @@ function FormAutoCompleteChild() { } FormAutoCompleteChild.prototype = { classID : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"), QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]), _debug: false, _enabled: true, + _pendingSearch: null, /* * init * * Initializes the content-process side of the FormAutoComplete component, * and add a listener for the message that the parent process sends when * a result is produced. */ @@ -356,17 +363,19 @@ FormAutoCompleteChild.prototype = { autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) { // This function is deprecated }, autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) { this.log("autoCompleteSearchAsync"); - this._pendingListener = aListener; + if (this._pendingSearch) { + this.stopAutoCompleteSearch(); + } let rect = BrowserUtils.getElementBoundingScreenRect(aField); let window = aField.ownerDocument.defaultView; let topLevelDocshell = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell) .sameTypeRootTreeItem .QueryInterface(Ci.nsIDocShell); @@ -378,36 +387,45 @@ FormAutoCompleteChild.prototype = { inputName: aInputName, untrimmedSearchString: aUntrimmedSearchString, left: rect.left, top: rect.top, width: rect.width, height: rect.height }); - mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", - function searchFinished(message) { - mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished); - let result = new FormAutoCompleteResult( - null, - [{text: res} for (res of message.data.results)], - null, - null - ); - if (aListener) { - aListener.onSearchCompletion(result); - } + let search = this._pendingSearch = {}; + let searchFinished = message => { + mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished); + + // Check whether stopAutoCompleteSearch() was called, i.e. the search + // was cancelled, while waiting for a result. + if (search != this._pendingSearch) { + return; } - ); + this._pendingSearch = null; + let result = new FormAutoCompleteResult( + null, + [for (res of message.data.results) {text: res}], + null, + null + ); + if (aListener) { + aListener.onSearchCompletion(result); + } + } + + mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished); this.log("autoCompleteSearchAsync message was sent"); }, stopAutoCompleteSearch : function () { - this.log("stopAutoCompleteSearch"); + this.log("stopAutoCompleteSearch"); + this._pendingSearch = null; }, }; // end of FormAutoCompleteChild implementation // nsIAutoCompleteResult implementation function FormAutoCompleteResult (formHistory, entries, fieldName, searchString) { this.formHistory = formHistory; this.entries = entries; this.fieldName = fieldName;
--- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -6566,26 +6566,26 @@ }, "DEVTOOLS_TABS_OPEN_PEAK_LINEAR": { "expires_in_version": "never", "kind": "linear", "high": "101", "n_buckets": 100, "description": "The peak number of open tabs in all windows for a session for devtools users." }, - "DEVTOOLS_TABS_OPEN_AVERAGE_EXPONENTIAL": { - "expires_in_version": "never", - "kind": "exponential", + "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": { + "expires_in_version": "never", + "kind": "linear", "high": "101", "n_buckets": "100", "description": "The mean number of open tabs in all windows for a session for devtools users." }, - "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL": { - "expires_in_version": "never", - "kind": "exponential", + "DEVTOOLS_TABS_PINNED_PEAK_LINEAR": { + "expires_in_version": "never", + "kind": "linear", "high": "101", "n_buckets": "100", "description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users." }, "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR": { "expires_in_version": "never", "kind": "linear", "high": "101", @@ -7282,16 +7282,24 @@ }, "LOOP_TWO_WAY_MEDIA_CONN_LENGTH": { "expires_in_version": "43", "kind": "count", "keyed": true, "releaseChannelCollection": "opt-out", "description": "Connection length for bi-directionally connected media" }, + "LOOP_SHARING_STATE_CHANGE": { + "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"], + "expires_in_version": "43", + "kind": "count", + "keyed": true, + "releaseChannelCollection": "opt-in", + "description": "Number of times the sharing feature has been enabled and disabled" + }, "E10S_AUTOSTART": { "expires_in_version": "never", "kind": "boolean", "description": "Whether a session is set to autostart e10s windows" }, "E10S_WINDOW": { "expires_in_version": "never", "kind": "boolean",
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm +++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm @@ -64,35 +64,68 @@ this.__defineSetter__("_maxUsedThemes", }); // Holds the ID of the theme being enabled or disabled while sending out the // events so cached AddonWrapper instances can return correct values for // permissions and pendingOperations var _themeIDBeingEnabled = null; var _themeIDBeingDisabled = null; +// Convert from the old storage format (in which the order of usedThemes +// was combined with isThemeSelected to determine which theme was selected) +// to the new one (where a selectedThemeID determines which theme is selected). +(function migrateToNewStorageFormat() { + let wasThemeSelected = false; + try { + wasThemeSelected = _prefs.getBoolPref("isThemeSelected"); + } catch(e) { } + + if (wasThemeSelected) { + _prefs.clearUserPref("isThemeSelected"); + let themes = []; + try { + themes = JSON.parse(_prefs.getComplexValue("usedThemes", + Ci.nsISupportsString).data); + } catch (e) { } + + if (Array.isArray(themes) && themes[0]) { + _prefs.setCharPref("selectedThemeID", themes[0].id); + } + } +})(); + this.LightweightThemeManager = { get name() "LightweightThemeManager", + // Themes that can be added for an application. They can't be removed, and + // will always show up at the top of the list. + _builtInThemes: new Map(), + get usedThemes () { + let themes = []; try { - return JSON.parse(_prefs.getComplexValue("usedThemes", - Ci.nsISupportsString).data); - } catch (e) { - return []; - } + themes = JSON.parse(_prefs.getComplexValue("usedThemes", + Ci.nsISupportsString).data); + } catch (e) { } + + themes.push(...this._builtInThemes.values()); + return themes; }, get currentTheme () { + let selectedThemeID = null; try { - if (_prefs.getBoolPref("isThemeSelected")) - var data = this.usedThemes[0]; + selectedThemeID = _prefs.getCharPref("selectedThemeID"); } catch (e) {} - return data || null; + let data = null; + if (selectedThemeID) { + data = this.getUsedTheme(selectedThemeID); + } + return data; }, get currentThemeForDisplay () { var data = this.currentTheme; if (data && PERSIST_ENABLED) { for (let key in PERSIST_FILES) { try { @@ -120,32 +153,56 @@ this.LightweightThemeManager = { if (usedTheme.id == aId) return usedTheme; } return null; }, forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) { let theme = this.getUsedTheme(aId); - if (!theme) + if (!theme || LightweightThemeManager._builtInThemes.has(theme.id)) return; let wrapper = new AddonWrapper(theme); AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); var currentTheme = this.currentTheme; if (currentTheme && currentTheme.id == aId) { this.themeChanged(null); AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false); } _updateUsedThemes(_usedThemesExceptId(aId)); AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); }, + addBuiltInTheme: function LightweightThemeManager_addBuiltInTheme(theme) { + if (!theme || !theme.id || this.usedThemes.some(t => t.id == theme.id)) { + throw new Error("Trying to add invalid builtIn theme"); + } + + this._builtInThemes.set(theme.id, theme); + }, + + forgetBuiltInTheme: function LightweightThemeManager_forgetBuiltInTheme(id) { + if (!this._builtInThemes.has(id)) { + let currentTheme = this.currentTheme; + if (currentTheme && currentTheme.id == id) { + this.currentTheme = null; + } + } + return this._builtInThemes.delete(id); + }, + + clearBuiltInThemes: function LightweightThemeManager_clearBuiltInThemes() { + for (let id of this._builtInThemes.keys()) { + this.forgetBuiltInTheme(id); + } + }, + previewTheme: function LightweightThemeManager_previewTheme(aData) { let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); cancel.data = false; Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested", JSON.stringify(aData)); if (cancel.data) return; @@ -237,17 +294,21 @@ this.LightweightThemeManager = { if (PERSIST_ENABLED) { LightweightThemeImageOptimizer.purge(); _persistImages(aData, function themeChanged_persistImages() { _notifyWindows(this.currentThemeForDisplay); }.bind(this)); } } - _prefs.setBoolPref("isThemeSelected", aData != null); + if (aData) + _prefs.setCharPref("selectedThemeID", aData.id); + else + _prefs.clearUserPref("selectedThemeID"); + _notifyWindows(aData); Services.obs.notifyObservers(null, "lightweight-theme-changed", null); }, /** * Starts the Addons provider and enables the new lightweight theme if * necessary. */ @@ -457,17 +518,21 @@ function AddonWrapper(aTheme) { this.__defineGetter__("size", function AddonWrapper_sizeGetter() { // The size changes depending on whether the theme is in use or not, this is // probably not worth exposing. return null; }); this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() { - let permissions = AddonManager.PERM_CAN_UNINSTALL; + let permissions = 0; + + // Do not allow uninstall of builtIn themes. + if (!LightweightThemeManager._builtInThemes.has(aTheme.id)) + permissions = AddonManager.PERM_CAN_UNINSTALL; if (this.userDisabled) permissions |= AddonManager.PERM_CAN_ENABLE; else permissions |= AddonManager.PERM_CAN_DISABLE; return permissions; }); this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() { @@ -674,16 +739,19 @@ function _usedThemesExceptId(aId) function _version(aThemeData) aThemeData.version || ""; function _makeURI(aURL, aBaseURI) Services.io.newURI(aURL, null, aBaseURI); function _updateUsedThemes(aList) { + // Remove app-specific themes before saving them to the usedThemes pref. + aList = aList.filter(theme => !LightweightThemeManager._builtInThemes.has(theme.id)); + // Send uninstall events for all themes that need to be removed. while (aList.length > _maxUsedThemes) { let wrapper = new AddonWrapper(aList[aList.length - 1]); AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); aList.pop(); AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js @@ -14,28 +14,30 @@ function dummy(id) { name: Math.random().toString(), headerURL: "http://lwttest.invalid/a.png", footerURL: "http://lwttest.invalid/b.png", textcolor: Math.random().toString(), accentcolor: Math.random().toString() }; } +function hasPermission(aAddon, aPerm) { + var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()]; + return !!(aAddon.permissions & perm); +} + function run_test() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); startupManager(); Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 8); - var temp = {}; - Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", temp); - do_check_eq(typeof temp.LightweightThemeManager, "object"); + let {LightweightThemeManager: ltm} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {}); - var ltm = temp.LightweightThemeManager; - + do_check_eq(typeof ltm, "object"); do_check_eq(typeof ltm.usedThemes, "object"); do_check_eq(ltm.usedThemes.length, 0); do_check_eq(ltm.currentTheme, null); ltm.previewTheme(dummy("preview0")); do_check_eq(ltm.usedThemes.length, 0); do_check_eq(ltm.currentTheme, null); @@ -506,9 +508,91 @@ function run_test() { ltm.currentTheme = dummy("x33"); do_check_eq(ltm.usedThemes.length, 32); Services.prefs.clearUserPref("lightweightThemes.maxUsedThemes"); do_check_eq(ltm.usedThemes.length, 30); + + let usedThemes = ltm.usedThemes; + for (let theme of usedThemes) { + ltm.forgetUsedTheme(theme.id); + } + + // Check builtInTheme functionality for Bug 1094821 + do_check_eq(ltm._builtInThemes.toString(), "[object Map]"); + do_check_eq([...ltm._builtInThemes.entries()].length, 0); + do_check_eq(ltm.usedThemes.length, 0); + + ltm.addBuiltInTheme(dummy("builtInTheme0")); + do_check_eq([...ltm._builtInThemes].length, 1); + do_check_eq(ltm.usedThemes.length, 1); + do_check_eq(ltm.usedThemes[0].id, "builtInTheme0"); + + ltm.addBuiltInTheme(dummy("builtInTheme1")); + do_check_eq([...ltm._builtInThemes].length, 2); + do_check_eq(ltm.usedThemes.length, 2); + do_check_eq(ltm.usedThemes[1].id, "builtInTheme1"); + + // Clear all and then re-add + ltm.clearBuiltInThemes(); + do_check_eq([...ltm._builtInThemes].length, 0); + do_check_eq(ltm.usedThemes.length, 0); + + ltm.addBuiltInTheme(dummy("builtInTheme0")); + ltm.addBuiltInTheme(dummy("builtInTheme1")); + do_check_eq([...ltm._builtInThemes].length, 2); + do_check_eq(ltm.usedThemes.length, 2); + + do_test_pending(); + + AddonManager.getAddonByID("builtInTheme0@personas.mozilla.org", aAddon => { + // App specific theme can't be uninstalled or disabled, + // but can be enabled (since it isn't already applied). + do_check_eq(hasPermission(aAddon, "uninstall"), false); + do_check_eq(hasPermission(aAddon, "disable"), false); + do_check_eq(hasPermission(aAddon, "enable"), true); + + ltm.currentTheme = dummy("x0"); + do_check_eq([...ltm._builtInThemes].length, 2); + do_check_eq(ltm.usedThemes.length, 3); + do_check_eq(ltm.usedThemes[0].id, "x0"); + do_check_eq(ltm.currentTheme.id, "x0"); + do_check_eq(ltm.usedThemes[1].id, "builtInTheme0"); + do_check_eq(ltm.usedThemes[2].id, "builtInTheme1"); + + Assert.throws(() => { ltm.addBuiltInTheme(dummy("builtInTheme0")) }, + "Exception is thrown adding a duplicate theme"); + Assert.throws(() => { ltm.addBuiltInTheme("not a theme object") }, + "Exception is thrown adding an invalid theme"); + + AddonManager.getAddonByID("x0@personas.mozilla.org", aAddon => { + // Currently applied (non-app-specific) can be uninstalled or disabled, + // but can't be enabled (since it's already applied). + do_check_eq(hasPermission(aAddon, "uninstall"), true); + do_check_eq(hasPermission(aAddon, "disable"), true); + do_check_eq(hasPermission(aAddon, "enable"), false); + + ltm.forgetUsedTheme("x0"); + do_check_eq(ltm.currentTheme, null); + + // Removing the currently applied app specific theme should unapply it + ltm.currentTheme = ltm.getUsedTheme("builtInTheme0"); + do_check_eq(ltm.currentTheme.id, "builtInTheme0"); + do_check_true(ltm.forgetBuiltInTheme("builtInTheme0")); + do_check_eq(ltm.currentTheme, null); + + do_check_eq([...ltm._builtInThemes].length, 1); + do_check_eq(ltm.usedThemes.length, 1); + + do_check_true(ltm.forgetBuiltInTheme("builtInTheme1")); + do_check_false(ltm.forgetBuiltInTheme("not-an-existing-theme-id")); + + do_check_eq([...ltm._builtInThemes].length, 0); + do_check_eq(ltm.usedThemes.length, 0); + do_check_eq(ltm.currentTheme, null); + + do_test_finished(); + }); + }); }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js @@ -113,17 +113,17 @@ function run_test() { description: "A test theme", author: "Mozilla", homepageURL: "http://localhost/data/index.html", headerURL: "http://localhost/data/header.png", footerURL: "http://localhost/data/footer.png", previewURL: "http://localhost/data/preview.png", iconURL: "http://localhost/data/icon.png" }])); - Services.prefs.setBoolPref("lightweightThemes.isThemeSelected", true); + Services.prefs.setCharPref("lightweightThemes.selectedThemeID", "1"); let stagedXPIs = profileDir.clone(); stagedXPIs.append("staged-xpis"); stagedXPIs.append("addon6@tests.mozilla.org"); stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755); let addon6 = do_get_addon("test_migrate6"); addon6.copyTo(stagedXPIs, "tmp.xpi");
--- a/toolkit/themes/windows/global/aboutReader.css +++ b/toolkit/themes/windows/global/aboutReader.css @@ -252,16 +252,17 @@ body { background-repeat: no-repeat; background-color: transparent; height: 40px; width: 40px; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #c1c1c1; + padding: 0; } .button[hidden] { display: none; } .dropdown { text-align: center;