author | Ryan VanderMeulen <ryanvm@gmail.com> |
Wed, 10 Sep 2014 18:36:26 -0400 | |
changeset 227917 | bc7deafdac4b8553ec45e3111e3fa38ef4de3eb0 |
parent 227916 | 280ac54e2dd89dfcb80fc28eaf66f42148f91476 (current diff) |
parent 227823 | bad6a9fc2bf07d1ad360b037233bb6d84a2b3e8e (diff) |
child 227956 | 0ef5e1a0486ab9a478fb8a3edea62fdacf4a8a56 |
child 227958 | ed2fb19942d0846fe15acbf69b99dda4fa46d453 |
child 228025 | 7962b4aaceb5a9413582cf3f0389863bc3394da5 |
child 228043 | 2a6be99b3452cfebd44759dd5f36e28cbdbcefb9 |
push id | 4187 |
push user | bhearsum@mozilla.com |
push date | Fri, 28 Nov 2014 15:29:12 +0000 |
treeherder | mozilla-beta@f23cc6a30c11 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 35.0a1 |
first release with | nightly linux32
bc7deafdac4b
/
35.0a1
/
20140911030204
/
files
nightly linux64
bc7deafdac4b
/
35.0a1
/
20140911030204
/
files
nightly mac
bc7deafdac4b
/
35.0a1
/
20140911030204
/
files
nightly win32
bc7deafdac4b
/
35.0a1
/
20140911030204
/
files
nightly win64
bc7deafdac4b
/
35.0a1
/
20140911030204
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
35.0a1
/
20140911030204
/
pushlog to previous
nightly linux64
35.0a1
/
20140911030204
/
pushlog to previous
nightly mac
35.0a1
/
20140911030204
/
pushlog to previous
nightly win32
35.0a1
/
20140911030204
/
pushlog to previous
nightly win64
35.0a1
/
20140911030204
/
pushlog to previous
|
browser/app/profile/firefox.js | file | annotate | diff | comparison | revisions | |
browser/devtools/shared/test/browser_graphs-07.js | file | annotate | diff | comparison | revisions | |
dom/bindings/BindingUtils.h | file | annotate | diff | comparison | revisions |
--- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -10,17 +10,17 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> @@ -33,17 +33,17 @@ <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="83760d213fb3bec7b4117d266fcfbf6fe2ba14ab"/> <project name="device/common" path="device/common" revision="6a2995683de147791e516aae2ccb31fdfbe2ad30"/> <project name="device/sample" path="device/sample" revision="1a3d8efa0ad32ec8f145367a3cf0f54b97385c3c"/> <project name="platform/abi/cpp" path="abi/cpp" revision="18f1b5e28734183ff8073fe86dc46bc4ebba8a59"/> <project name="platform/bionic" path="bionic" revision="86b1f589c313422a7da1812512b9ec8d1cf9ba3c"/> <project name="platform/bootable/recovery" path="bootable/recovery" revision="4eece0d80928a2b5266b78421ebf0c8686d4ad2c"/> <project name="platform/external/aac" path="external/aac" revision="fa3eba16446cc8f2f5e2dfc20d86a49dbd37299e"/> <project name="platform/external/bison" path="external/bison" revision="c2418b886165add7f5a31fc5609f0ce2d004a90e"/> - <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="c50830cae1b748024eec7e73ad98a4e427f663c7"/> + <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="c8e99ca7e11c00f8124196fe1726a15e6e976587"/> <project name="platform/external/bsdiff" path="external/bsdiff" revision="23e322ab19fb7d74c2c37e40ce364d9f709bdcee"/> <project name="platform/external/bzip2" path="external/bzip2" revision="1cb636bd8e9e5cdfd5d5b2909a122f6e80db62de"/> <project name="platform/external/checkpolicy" path="external/checkpolicy" revision="0d73ef7049feee794f14cf1af88d05dae8139914"/> <project name="platform/external/dhcpcd" path="external/dhcpcd" revision="84b7252b0a9d0edc9a1db1e0c518771d26b23058"/> <project name="platform/external/dnsmasq" path="external/dnsmasq" revision="41d356427a632f5336384bfa45c8420ffc274f66"/> <project name="platform/external/dropbear" path="external/dropbear" revision="a34ddbe3819bc465968f3676c734b405e655f8b7"/> <project name="platform/external/e2fsprogs" path="external/e2fsprogs" revision="47478a2944a2a17c7fdebe9d92573db92013125c"/> <project name="platform/external/elfutils" path="external/elfutils" revision="b23b2dfb354b3ccf5d1c5d39815f02e7048cf516"/>
--- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -10,21 +10,21 @@ <!--original fetch url was git://github.com/mozilla/--> <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/> <!-- Gonk specific things and forks --> - <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900"> + <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/> @@ -125,16 +125,16 @@ <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/> <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/> <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/> <!-- Emulator specific things --> <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/> <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/> <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/> <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/> - <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="c7ccf6eff27f99e39a9eca94cde48aaece5e47db"/> + <project name="libnfcemu" path="external/libnfcemu" remote="b2g" revision="125ccf9bd5986c7728ea44508b3e1d1185ac028b"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d259117b4976decbe2f76eeed85218bf0109190f"/> <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/> <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="54a712b46fe937dacdaed9b1261c63847129a719"/> <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/> <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/> </manifest>
--- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -10,17 +10,17 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/>
--- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -10,21 +10,21 @@ <!--original fetch url was git://github.com/mozilla/--> <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/> <!-- Gonk specific things and forks --> - <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900"> + <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -10,17 +10,17 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> @@ -127,17 +127,17 @@ <remove-project name="platform/hardware/libhardware"/> <remove-project name="platform/external/bluetooth/bluedroid"/> <!--original fetch url was git://github.com/t2m-foxfone/--> <remote fetch="https://git.mozilla.org/external/t2m-foxfone" name="t2m"/> <default remote="caf" revision="LNX.LA.3.5.2.1.1" sync-j="4"/> <!-- Flame specific things --> <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="1bb28abbc215f45220620af5cd60a8ac1be93722"/> <project name="device/qcom/common" path="device/qcom/common" revision="54c32c2ddef066fbdf611d29e4b7c47e0363599e"/> - <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="540314ae9c56394c6b1f17a267db9f25c5acb9d6"/> + <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="8f988f3950da8d55676b3b77b09d5722b967e07b"/> <project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="893238eb1215f8fd4f3747169170cc5e1cc33969"/> <project name="kernel_lk" path="bootable/bootloader/lk" remote="b2g" revision="9e62af4da848d56841bdde326f9bba26c743c33a"/> <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="082a1f98422e6a6b56f61218d6fcf465e85d4c58"/> <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="5b71e40213f650459e95d35b6f14af7e88d8ab62"/> <project name="platform_external_libnfc-nci" path="external/libnfc-nci" remote="t2m" revision="4186bdecb4dae911b39a8202252cc2310d91b0be"/> <project name="platform/frameworks/av" path="frameworks/av" revision="c1814713bd2d07c2af0c236007badc8732a34324"/> <project name="platform/frameworks/base" path="frameworks/base" revision="6b58ab45e3e56c1fc20708cc39fa2264c52558df"/> <project name="platform/frameworks/native" path="frameworks/native" revision="a46a9f1ac0ed5662d614c277cbb14eb3f332f365"/>
--- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { "git_revision": "", "remote": "", "branch": "" }, - "revision": "a4a76a4221d7d963d01377f38d68768d0e829017", + "revision": "6465db9982731ec95ad344901af20086ad94291f", "repo_path": "/integration/gaia-central" }
--- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -8,21 +8,21 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/> <!-- Gonk specific things and forks --> - <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900"> + <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/> <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -6,21 +6,21 @@ <!--original fetch url was git://github.com/mozilla/--> <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/> <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/> <!-- Gonk specific things and forks --> - <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900"> + <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/> <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -8,21 +8,21 @@ <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/> <!-- Gonk specific things and forks --> - <project name="platform_build" path="build" remote="b2g" revision="700b031a54079f791344aa091798f6b43a9e2900"> + <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="f108c706fae43cd61628babdd9463e7695b2496e"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="7f21bdda274f0329393ef0e5a9374c06255c6f57"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="facdb3593e63dcbb740709303a5b2527113c50a0"/> <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1365,18 +1365,19 @@ pref("devtools.debugger.tracer", false); // The default Debugger UI settings pref("devtools.debugger.ui.panes-sources-width", 200); pref("devtools.debugger.ui.panes-instruments-width", 300); pref("devtools.debugger.ui.panes-visible-on-startup", false); pref("devtools.debugger.ui.variables-sorting-enabled", true); pref("devtools.debugger.ui.variables-only-enum-visible", false); pref("devtools.debugger.ui.variables-searchbox-visible", false); -// Enable the Profiler +// Enable the Profiler and the Timeline pref("devtools.profiler.enabled", true); +pref("devtools.timeline.enabled", false); // The default Profiler UI settings pref("devtools.profiler.ui.show-platform-data", false); // The default cache UI setting pref("devtools.cache.disabled", false); // Enable the Network Monitor
--- a/browser/base/content/browser-syncui.js +++ b/browser/base/content/browser-syncui.js @@ -1,12 +1,21 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +#ifdef MOZ_SERVICES_CLOUDSYNC +XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", + "resource://gre/modules/CloudSync.jsm"); +#else +let CloudSync = null; +#endif + // gSyncUI handles updating the tools menu and displaying notifications. let gSyncUI = { DEFAULT_EOL_URL: "https://www.mozilla.org/firefox/?utm_source=synceol", _obs: ["weave:service:sync:start", "weave:service:quota:remaining", "weave:service:setup-complete", "weave:service:login:start", @@ -117,17 +126,19 @@ let gSyncUI = { let needsSetup = this._needsSetup(); let loginFailed = this._loginFailed(); // Start off with a clean slate document.getElementById("sync-reauth-state").hidden = true; document.getElementById("sync-setup-state").hidden = true; document.getElementById("sync-syncnow-state").hidden = true; - if (loginFailed) { + if (CloudSync && CloudSync.ready && CloudSync().adapters.count) { + document.getElementById("sync-syncnow-state").hidden = false; + } else if (loginFailed) { document.getElementById("sync-reauth-state").hidden = false; } else if (needsSetup) { document.getElementById("sync-setup-state").hidden = false; } else { document.getElementById("sync-syncnow-state").hidden = false; } if (!gBrowser) @@ -270,17 +281,24 @@ let gSyncUI = { openServerStatus: function () { let statusURL = Services.prefs.getCharPref("services.sync.statusURL"); window.openUILinkIn(statusURL, "tab"); }, // Commands doSync: function SUI_doSync() { - setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0); + let needsSetup = this._needsSetup(); + let loginFailed = this._loginFailed(); + + if (!(loginFailed || needsSetup)) { + setTimeout(function () Weave.Service.errorHandler.syncAndReportErrors(), 0); + } + + Services.obs.notifyObservers(null, "cloudsync:user-sync", null); }, handleToolbarButton: function SUI_handleStatusbarButton() { if (this._needsSetup()) this.openSetup(); else this.doSync(); },
--- a/browser/base/content/test/general/browser_bug575561.js +++ b/browser/base/content/test/general/browser_bug575561.js @@ -1,84 +1,84 @@ -function test() { - waitForExplicitFinish(); - - // Pinned: Link to the same domain should not open a new tab - // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html - testLink(0, true, false, function() { - // Pinned: Link to a different subdomain should open a new tab - // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html - testLink(1, true, true, function() { - // Pinned: Link to a different domain should open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html - testLink(2, true, true, function() { - // Not Pinned: Link to a different domain should not open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html - testLink(2, false, false, function() { - // Pinned: Targetted link should open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo" - testLink(3, true, true, function() { - // Pinned: Link in a subframe should not open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe - testLink(0, true, false, function() { - // Pinned: Link to the same domain (with www prefix) should not open a new tab - // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html - testLink(4, true, false, function() { - // Pinned: Link to a data: URI should not open a new tab - // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html> - testLink(5, true, false, function() { - // Pinned: Link to an about: URI should not open a new tab - // Tests link to about:mozilla - testLink(6, true, false, finish); - }); - }); - }, true); - }); - }); - }); - }); - }); -} - -function testLink(aLinkIndex, pinTab, expectNewTab, nextTest, testSubFrame) { - let appTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/general/app_bug575561.html", {skipAnimation: true}); - if (pinTab) - gBrowser.pinTab(appTab); - gBrowser.selectedTab = appTab; - - waitForDocLoadComplete(appTab.linkedBrowser).then(function() { - let browser = gBrowser.getBrowserForTab(appTab); - if (testSubFrame) - browser = browser.contentDocument.getElementsByTagName("iframe")[0]; - - let links = browser.contentDocument.getElementsByTagName("a"); - - if (expectNewTab) - gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true); - else - waitForDocLoadComplete(appTab.linkedBrowser).then(onPageLoad); - - info("Clicking " + links[aLinkIndex].textContent); - EventUtils.sendMouseEvent({type:"click"}, links[aLinkIndex], browser.contentWindow); - let linkLocation = links[aLinkIndex].href; - - function onPageLoad() { - browser.removeEventListener("load", onPageLoad, true); - is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab"); - executeSoon(function(){ - gBrowser.removeTab(appTab); - nextTest(); - }); - } - - function onTabOpen(event) { - gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true); - ok(true, "Link should open a new tab"); - waitForDocLoadComplete(event.target.linkedBrowser).then(function() { - executeSoon(function(){ - gBrowser.removeTab(appTab); - gBrowser.removeCurrentTab(); - nextTest(); - }); - }); - } - }); -} +const TEST_URL = "http://example.com/browser/browser/base/content/test/general/app_bug575561.html"; + +add_task(function*() { + SimpleTest.requestCompleteLog(); + + // Pinned: Link to the same domain should not open a new tab + // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html + yield testLink(0, true, false); + // Pinned: Link to a different subdomain should open a new tab + // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html + yield testLink(1, true, true); + + // Pinned: Link to a different domain should open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html + yield testLink(2, true, true); + + // Not Pinned: Link to a different domain should not open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html + yield testLink(2, false, false); + + // Pinned: Targetted link should open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo" + yield testLink(3, true, true); + + // Pinned: Link in a subframe should not open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe + yield testLink(0, true, false, true); + + // Pinned: Link to the same domain (with www prefix) should not open a new tab + // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html + yield testLink(4, true, false); + + // Pinned: Link to a data: URI should not open a new tab + // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html> + yield testLink(5, true, false); + + // Pinned: Link to an about: URI should not open a new tab + // Tests link to about:mozilla + yield testLink(6, true, false); +}); + +let waitForPageLoad = Task.async(function*(browser, linkLocation) { + yield waitForDocLoadComplete(); + + is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab"); +}); + +let waitForTabOpen = Task.async(function*() { + let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true); + ok(true, "Link should open a new tab"); + + yield waitForDocLoadComplete(event.target.linkedBrowser); + yield Promise.resolve(); + + gBrowser.removeCurrentTab(); +}); + +let testLink = Task.async(function*(aLinkIndex, pinTab, expectNewTab, testSubFrame) { + let appTab = gBrowser.addTab(TEST_URL, {skipAnimation: true}); + if (pinTab) + gBrowser.pinTab(appTab); + gBrowser.selectedTab = appTab; + + yield waitForDocLoadComplete(); + + let browser = appTab.linkedBrowser; + if (testSubFrame) + browser = browser.contentDocument.querySelector("iframe"); + + let link = browser.contentDocument.querySelectorAll("a")[aLinkIndex]; + + let promise; + if (expectNewTab) + promise = waitForTabOpen(); + else + promise = waitForPageLoad(browser, link.href); + + info("Clicking " + link.textContent); + link.click(); + + yield promise; + + gBrowser.removeTab(appTab); +});
--- a/browser/base/content/test/general/head.js +++ b/browser/base/content/test/general/head.js @@ -103,16 +103,29 @@ function waitForCondition(condition, nex } function promiseWaitForCondition(aConditionFn) { let deferred = Promise.defer(); waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass."); return deferred.promise; } +function promiseWaitForEvent(object, eventName, capturing = false) { + return new Promise((resolve) => { + function listener(event) { + info("Saw " + eventName); + object.removeEventListener(eventName, listener, capturing); + resolve(event); + } + + info("Waiting for " + eventName); + object.addEventListener(eventName, listener, capturing); + }); +} + function getTestPlugin(aName) { var pluginName = aName || "Test Plug-in"; var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); var tags = ph.getPluginTags(); // Find the test plugin for (var i = 0; i < tags.length; i++) { if (tags[i].name == pluginName) @@ -438,16 +451,17 @@ function waitForDocLoadAndStopIt(aExpect * @return promise */ function waitForDocLoadComplete(aBrowser=gBrowser) { let deferred = Promise.defer(); let progressListener = { onStateChange: function (webProgress, req, flags, status) { let docStart = Ci.nsIWebProgressListener.STATE_IS_NETWORK | Ci.nsIWebProgressListener.STATE_STOP; + info("Saw state " + flags.toString(16)); if ((flags & docStart) == docStart) { aBrowser.removeProgressListener(progressListener); info("Browser loaded"); deferred.resolve(); } }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
--- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -117,16 +117,33 @@ function injectLoopAPI(targetWindow) { locale: { enumerable: true, get: function() { return MozLoopService.locale; } }, /** + * Returns the callData for a specific callDataId + * + * The data was retrieved from the LoopServer via a GET/calls/<version> request + * triggered by an incoming message from the LoopPushServer. + * + * @param {int} loopCallId + * @returns {callData} The callData or undefined if error. + */ + getCallData: { + enumerable: true, + writable: true, + value: function(loopCallId) { + return Cu.cloneInto(MozLoopService.getCallData(loopCallId), targetWindow); + } + }, + + /** * Returns the contacts API. * * @returns {Object} The contacts API object */ contacts: { enumerable: true, get: function() { if (contactsAPI) { @@ -333,25 +350,34 @@ function injectLoopAPI(targetWindow) { * @param {Object} payloadObj An object which is converted to JSON and * transmitted with the request. * @param {Function} callback Called when the request completes. */ hawkRequest: { enumerable: true, writable: true, value: function(path, method, payloadObj, callback) { + // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST // XXX Should really return a DOM promise here. - return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => { + return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => { callback(null, response.body); }, (error) => { callback(Cu.cloneInto(error, targetWindow)); }); } }, + LOOP_SESSION_TYPE: { + enumerable: true, + writable: false, + value: function() { + return LOOP_SESSION_TYPE; + }, + }, + logInToFxA: { enumerable: true, writable: true, value: function() { return MozLoopService.logInToFxA(); } },
--- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -10,24 +10,29 @@ const { classes: Cc, interfaces: Ci, uti // https://github.com/mozilla-services/loop-server/blob/45787d34108e2f0d87d74d4ddf4ff0dbab23501c/loop/errno.json#L6 const INVALID_AUTH_TOKEN = 110; // Ticket numbers are 24 bits in length. // The highest valid ticket number is 16777214 (2^24 - 2), so that a "now // serving" number of 2^24 - 1 is greater than it. const MAX_SOFT_START_TICKET_NUMBER = 16777214; +const LOOP_SESSION_TYPE = { + GUEST: 1, + FXA: 2, +}; + Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); -this.EXPORTED_SYMBOLS = ["MozLoopService"]; +this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE"]; XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport", @@ -76,16 +81,18 @@ let gErrors = new Map(); /** * Internal helper methods and state * * The registration is a two-part process. First we need to connect to * and register with the push server. Then we need to take the result of that * and register with the Loop server. */ let MozLoopServiceInternal = { + callsData: {data: undefined}, + // The uri of the Loop server. get loopServerUri() Services.prefs.getCharPref("loop.server"), /** * The initial delay for push registration. This ensures we don't start * kicking off straight after browser startup, just a few seconds later. */ get initialRegistrationDelayMilliseconds() { @@ -197,34 +204,36 @@ let MozLoopServiceInternal = { this.onHandleNotification.bind(this)); return result; }, /** * Performs a hawk based request to the loop server. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * This is one of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and * transmitted with the request. * @returns {Promise} * Returns a promise that resolves to the response of the API call, * or is rejected with an error. If the server response can be parsed * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(path, method, payloadObj) { + hawkRequest: function(sessionType, path, method, payloadObj) { if (!gHawkClient) { gHawkClient = new HawkClient(this.loopServerUri); } let sessionToken; try { - sessionToken = Services.prefs.getCharPref("loop.hawk-session-token"); + sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType)); } catch (x) { // It is ok for this not to exist, we'll default to sending no-creds } let credentials; if (sessionToken) { // true = use a hex key, as required by the server (see bug 1032738). credentials = deriveHawkCredentials(sessionToken, "sessionToken", @@ -232,29 +241,47 @@ let MozLoopServiceInternal = { } return gHawkClient.request(path, method, credentials, payloadObj).catch(error => { console.error("Loop hawkRequest error:", error); throw error; }); }, + getSessionTokenPrefName: function(sessionType) { + let suffix; + switch (sessionType) { + case LOOP_SESSION_TYPE.GUEST: + suffix = ""; + break; + case LOOP_SESSION_TYPE.FXA: + suffix = ".fxa"; + break; + default: + throw new Error("Unknown LOOP_SESSION_TYPE"); + break; + } + return "loop.hawk-session-token" + suffix; + }, + /** * Used to store a session token from a request if it exists in the headers. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * One of the LOOP_SESSION_TYPE members. * @param {Object} headers The request headers, which may include a * "hawk-session-token" to be saved. * @return true on success or no token, false on failure. */ - storeSessionToken: function(headers) { + storeSessionToken: function(sessionType, headers) { let sessionToken = headers["hawk-session-token"]; if (sessionToken) { // XXX should do more validation here if (sessionToken.length === 64) { - Services.prefs.setCharPref("loop.hawk-session-token", sessionToken); + Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken); } else { // XXX Bubble the precise details up to the UI somehow (bug 1013248). console.warn("Loop server sent an invalid session token"); gRegisteredDeferred.reject("session-token-wrong-size"); gRegisteredDeferred = null; return false; } } @@ -269,59 +296,70 @@ let MozLoopServiceInternal = { */ onPushRegistered: function(err, pushUrl) { if (err) { gRegisteredDeferred.reject(err); gRegisteredDeferred = null; return; } - this.registerWithLoopServer(pushUrl); + this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => { + // storeSessionToken could have rejected and nulled the promise if the token was malformed. + if (!gRegisteredDeferred) { + return; + } + gRegisteredDeferred.resolve(); + // No need to clear the promise here, everything was good, so we don't need + // to re-register. + }, (error) => { + Cu.reportError("Failed to register with Loop server: " + error.errno); + gRegisteredDeferred.reject(error.errno); + gRegisteredDeferred = null; + }); }, /** - * Registers with the Loop server. + * Registers with the Loop server either as a guest or a FxA user. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA * @param {String} pushUrl The push url given by the push server. - * @param {Boolean} noRetry Optional, don't retry if authentication fails. + * @param {Boolean} [retry=true] Whether to retry if authentication fails. + * @return {Promise} */ - registerWithLoopServer: function(pushUrl, noRetry) { - this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl}) + registerWithLoopServer: function(sessionType, pushUrl, retry = true) { + return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURL: pushUrl}) .then((response) => { // If this failed we got an invalid token. storeSessionToken rejects // the gRegisteredDeferred promise for us, so here we just need to // early return. - if (!this.storeSessionToken(response.headers)) + if (!this.storeSessionToken(sessionType, response.headers)) return; this.clearError("registration"); - gRegisteredDeferred.resolve(); - // No need to clear the promise here, everything was good, so we don't need - // to re-register. }, (error) => { // There's other errors than invalid auth token, but we should only do the reset // as a last resort. if (error.code === 401 && error.errno === INVALID_AUTH_TOKEN) { if (this.urlExpiryTimeIsInFuture()) { // XXX Should this be reported to the user is a visible manner? Cu.reportError("Loop session token is invalid, all previously " + "generated urls will no longer work."); } // Authorization failed, invalid token, we need to try again with a new token. - Services.prefs.clearUserPref("loop.hawk-session-token"); - this.registerWithLoopServer(pushUrl, true); - return; + Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType)); + if (retry) { + return this.registerWithLoopServer(sessionType, pushUrl, false); + } } // XXX Bubble the precise details up to the UI somehow (bug 1013248). Cu.reportError("Failed to register with the loop server. error: " + error); this.setError("registration", error); - gRegisteredDeferred.reject(error.errno); - gRegisteredDeferred = null; + throw error; } ); }, /** * Callback from MozLoopPushHandler - A push notification has been received from * the server. * @@ -332,19 +370,33 @@ let MozLoopServiceInternal = { return; } // We set this here as it is assumed that once the user receives an incoming // call, they'll have had enough time to see the terms of service. See // bug 1046039 for background. Services.prefs.setCharPref("loop.seenToS", "seen"); - this.openChatWindow(null, - this.localizedStrings["incoming_call_title2"].textContent, - "about:loopconversation#incoming/" + version); + /* Request the information on the new call(s) associated with this version. */ + this.hawkRequest(LOOP_SESSION_TYPE.GUEST, + "/calls?version=" + version, "GET").then(response => { + try { + let respData = JSON.parse(response.body); + if (respData.calls && respData.calls[0]) { + this.callsData.data = respData.calls[0]; + this.openChatWindow(null, + this.localizedStrings["incoming_call_title2"].textContent, + "about:loopconversation#incoming/" + version); + } else { + console.warn("Error: missing calls[] in response"); + } + } catch (err) { + console.warn("Error parsing calls info", err); + } + }); }, /** * A getter to obtain and store the strings for loop. This is structured * for use by l10n.js. * * @returns {Object} a map of element ids with attributes to set. */ @@ -504,17 +556,17 @@ let MozLoopServiceInternal = { }, /** * Fetch Firefox Accounts (FxA) OAuth parameters from the Loop Server. * * @return {Promise} resolved with the body of the hawk request for OAuth parameters. */ promiseFxAOAuthParameters: function() { - return this.hawkRequest("/fxa-oauth/params", "POST").then(response => { + return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/params", "POST").then(response => { return JSON.parse(response.body); }); }, /** * Get the OAuth client constructed with Loop OAauth parameters. * * @return {Promise} @@ -582,17 +634,17 @@ let MozLoopServiceInternal = { if (!code || !state) { throw new Error("promiseFxAOAuthToken: code and state are required."); } let payload = { code: code, state: state, }; - return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => { + return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => { return JSON.parse(response.body); }); }, /** * Called once gFxAOAuthClient fires onComplete. * * @param {Deferred} deferred used to resolve or reject the gFxAOAuthClientPromise @@ -837,16 +889,29 @@ this.MozLoopService = { return Services.prefs.getComplexValue("general.useragent.locale", Ci.nsISupportsString).data; } catch (ex) { return "en-US"; } }, /** + * Returns the callData for a specific callDataId + * + * The data was retrieved from the LoopServer via a GET/calls/<version> request + * triggered by an incoming message from the LoopPushServer. + * + * @param {int} loopCallId + * @return {callData} The callData or undefined if error. + */ + getCallData: function(loopCallId) { + return MozLoopServiceInternal.callsData.data; + }, + + /** * Set any character preference under "loop.". * * @param {String} prefName The name of the pref without the preceding "loop." * @param {String} value The value to set. * * Any errors thrown by the Mozilla pref API are logged to the console. */ setLoopCharPref: function(prefName, value) { @@ -916,33 +981,44 @@ this.MozLoopService = { return Promise.resolve(gFxAOAuthTokenData); } return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => { return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state); }).then(tokenData => { gFxAOAuthTokenData = tokenData; return tokenData; + }).then(tokenData => { + return gRegisteredDeferred.promise.then(Task.async(function*() { + if (gPushHandler.pushUrl) { + yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl); + } else { + throw new Error("No pushUrl for FxA registration"); + } + return gFxAOAuthTokenData; + })); }, error => { gFxAOAuthTokenData = null; throw error; }); }, /** * Performs a hawk based request to the loop server. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * One of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and * transmitted with the request. * @returns {Promise} * Returns a promise that resolves to the response of the API call, * or is rejected with an error. If the server response can be parsed * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(path, method, payloadObj) { - return MozLoopServiceInternal.hawkRequest(path, method, payloadObj); + hawkRequest: function(sessionType, path, method, payloadObj) { + return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj); }, }; Object.freeze(this.MozLoopService);
--- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -99,17 +99,16 @@ loop.Client = (function($) { * Internal handler for requesting a call url from the server. * * Callback parameters: * - err null on successful registration, non-null otherwise. * - callUrlData an object of the obtained call url data if successful: * -- callUrl: The url of the call * -- expiresAt: The amount of hours until expiry of the url * - * @param {String} simplepushUrl a registered Simple Push URL * @param {string} nickname the nickname of the future caller * @param {Function} cb Callback(err, callUrlData) */ _requestCallUrlInternal: function(nickname, cb) { this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname}, function (error, responseText) { if (error) { this._failureHandler(cb, error); @@ -183,45 +182,12 @@ loop.Client = (function($) { if (err) { cb(err); return; } this._requestCallUrlInternal(nickname, cb); }.bind(this)); }, - - /** - * Requests call information from the server for all calls since the - * given version. - * - * @param {String} version the version identifier from the push - * notification - * @param {Function} cb Callback(err, calls) - */ - requestCallsInfo: function(version, cb) { - // XXX It is likely that we'll want to move some of this to whatever - // opens the chat window, but we'll need to decide on this in bug 1002418 - if (!version) { - throw new Error("missing required parameter version"); - } - - this.mozLoop.hawkRequest("/calls?version=" + version, "GET", null, - function (error, responseText) { - if (error) { - this._failureHandler(cb, error); - return; - } - - try { - var callsData = JSON.parse(responseText); - - cb(null, this._validate(callsData, expectedCallProperties)); - } catch (err) { - console.log("Error requesting calls info", err); - cb(err); - } - }.bind(this)); - } }; return Client; })(jQuery);
--- a/browser/components/loop/content/js/conversation.js +++ b/browser/components/loop/content/js/conversation.js @@ -152,17 +152,17 @@ loop.conversation = (function(OT, mozL10 * Required options: * - {loop.shared.models.ConversationModel} conversation Conversation model. * - {loop.shared.models.NotificationCollection} notifications * * @type {loop.shared.router.BaseConversationRouter} */ var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ routes: { - "incoming/:version": "incoming", + "incoming/:callId": "incoming", "call/accept": "accept", "call/decline": "decline", "call/ongoing": "conversation", "call/declineAndBlock": "declineAndBlock", "call/feedback": "feedback" }, /** @@ -177,53 +177,44 @@ loop.conversation = (function(OT, mozL10 */ endCall: function() { this.navigate("call/feedback", {trigger: true}); }, /** * Incoming call route. * - * @param {String} loopVersion The version from the push notification, set - * by the router from the URL. + * @param {String} callId Identifier assigned by the LoopService + * to this incoming call. */ - incoming: function(loopVersion) { + incoming: function(callId) { navigator.mozLoop.startAlerting(); - this._conversation.set({loopVersion: loopVersion}); this._conversation.once("accept", function() { this.navigate("call/accept", {trigger: true}); }.bind(this)); this._conversation.once("decline", function() { this.navigate("call/decline", {trigger: true}); }.bind(this)); this._conversation.once("declineAndBlock", function() { this.navigate("call/declineAndBlock", {trigger: true}); }.bind(this)); this._conversation.once("call:incoming", this.startCall, this); this._conversation.once("change:publishedStream", this._checkConnected, this); this._conversation.once("change:subscribedStream", this._checkConnected, this); - this._client.requestCallsInfo(loopVersion, function(err, sessionData) { - if (err) { - console.error("Failed to get the sessionData", err); - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - this._notifications.errorL10n("cannot_start_call_session_not_ready"); - return; - } - - // XXX For incoming calls we might have more than one call queued. - // For now, we'll just assume the first call is the right information. - // We'll probably really want to be getting this data from the - // background worker on the desktop client. - // Bug 1032700 should fix this. - this._conversation.setIncomingSessionData(sessionData[0]); - - this._setupWebSocketAndCallView(); - }.bind(this)); + var callData = navigator.mozLoop.getCallData(callId); + if (!callData) { + console.error("Failed to get the call data"); + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + this._notifications.errorL10n("cannot_start_call_session_not_ready"); + return; + } + this._conversation.setIncomingSessionData(callData); + this._setupWebSocketAndCallView(); }, /** * Used to set up the web socket connection and navigate to the * call view if appropriate. */ _setupWebSocketAndCallView: function() { this._websocket = new loop.CallConnectionWebSocket({
--- a/browser/components/loop/content/js/conversation.jsx +++ b/browser/components/loop/content/js/conversation.jsx @@ -152,17 +152,17 @@ loop.conversation = (function(OT, mozL10 * Required options: * - {loop.shared.models.ConversationModel} conversation Conversation model. * - {loop.shared.models.NotificationCollection} notifications * * @type {loop.shared.router.BaseConversationRouter} */ var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({ routes: { - "incoming/:version": "incoming", + "incoming/:callId": "incoming", "call/accept": "accept", "call/decline": "decline", "call/ongoing": "conversation", "call/declineAndBlock": "declineAndBlock", "call/feedback": "feedback" }, /** @@ -177,53 +177,44 @@ loop.conversation = (function(OT, mozL10 */ endCall: function() { this.navigate("call/feedback", {trigger: true}); }, /** * Incoming call route. * - * @param {String} loopVersion The version from the push notification, set - * by the router from the URL. + * @param {String} callId Identifier assigned by the LoopService + * to this incoming call. */ - incoming: function(loopVersion) { + incoming: function(callId) { navigator.mozLoop.startAlerting(); - this._conversation.set({loopVersion: loopVersion}); this._conversation.once("accept", function() { this.navigate("call/accept", {trigger: true}); }.bind(this)); this._conversation.once("decline", function() { this.navigate("call/decline", {trigger: true}); }.bind(this)); this._conversation.once("declineAndBlock", function() { this.navigate("call/declineAndBlock", {trigger: true}); }.bind(this)); this._conversation.once("call:incoming", this.startCall, this); this._conversation.once("change:publishedStream", this._checkConnected, this); this._conversation.once("change:subscribedStream", this._checkConnected, this); - this._client.requestCallsInfo(loopVersion, function(err, sessionData) { - if (err) { - console.error("Failed to get the sessionData", err); - // XXX Not the ideal response, but bug 1047410 will be replacing - // this by better "call failed" UI. - this._notifications.errorL10n("cannot_start_call_session_not_ready"); - return; - } - - // XXX For incoming calls we might have more than one call queued. - // For now, we'll just assume the first call is the right information. - // We'll probably really want to be getting this data from the - // background worker on the desktop client. - // Bug 1032700 should fix this. - this._conversation.setIncomingSessionData(sessionData[0]); - - this._setupWebSocketAndCallView(); - }.bind(this)); + var callData = navigator.mozLoop.getCallData(callId); + if (!callData) { + console.error("Failed to get the call data"); + // XXX Not the ideal response, but bug 1047410 will be replacing + // this by better "call failed" UI. + this._notifications.errorL10n("cannot_start_call_session_not_ready"); + return; + } + this._conversation.setIncomingSessionData(callData); + this._setupWebSocketAndCallView(); }, /** * Used to set up the web socket connection and navigate to the * call view if appropriate. */ _setupWebSocketAndCallView: function() { this._websocket = new loop.CallConnectionWebSocket({
--- a/browser/components/loop/content/shared/js/models.js +++ b/browser/components/loop/content/shared/js/models.js @@ -13,20 +13,17 @@ loop.shared.models = (function(l10n) { * Conversation model. */ var ConversationModel = Backbone.Model.extend({ defaults: { connected: false, // Session connected flag ongoing: false, // Ongoing call flag callerId: undefined, // Loop caller id loopToken: undefined, // Loop conversation token - loopVersion: undefined, // Loop version for /calls/ information. This - // is the version received from the push - // notification and is used by the server to - // determine the pending calls + loopCallId: undefined, // LoopService id for incoming session sessionId: undefined, // OT session id sessionToken: undefined, // OT session token apiKey: undefined, // OT api key callId: undefined, // The callId on the server progressURL: undefined, // The websocket url to use for progress websocketToken: undefined, // The token to use for websocket auth, this is // stored as a hex string which is what the server // requires.
--- a/browser/components/loop/test/desktop-local/client_test.js +++ b/browser/components/loop/test/desktop-local/client_test.js @@ -177,57 +177,10 @@ describe("loop.Client", function() { client.requestCallUrl("foo", callback); sinon.assert.calledOnce(callback); sinon.assert.calledWithMatch(callback, sinon.match(function(err) { return /Invalid data received/.test(err.message); })); }); }); - - describe("#requestCallsInfo", function() { - it("should prevent launching a conversation when version is missing", - function() { - expect(function() { - client.requestCallsInfo(); - }).to.Throw(Error, /missing required parameter version/); - }); - - it("should perform a get on /calls", function() { - client.requestCallsInfo(42, callback); - - sinon.assert.calledOnce(hawkRequestStub); - sinon.assert.calledWith(hawkRequestStub, - "/calls?version=42", "GET", null); - - }); - - it("should request data for all calls", function() { - hawkRequestStub.callsArgWith(3, null, - '{"calls": [{"apiKey": "fake"}]}'); - - client.requestCallsInfo(42, callback); - - sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]); - }); - - it("should send an error when the request fails", function() { - hawkRequestStub.callsArgWith(3, fakeErrorRes); - - client.requestCallsInfo(42, callback); - - sinon.assert.calledWithMatch(callback, sinon.match(function(err) { - return /400.*invalid token/.test(err.message); - })); - }); - - it("should send an error if the data is not valid", function() { - hawkRequestStub.callsArgWith(3, null, "{}"); - - client.requestCallsInfo(42, callback); - - sinon.assert.calledWithMatch(callback, sinon.match(function(err) { - return /Invalid data received/.test(err.message); - })); - }); - }); }); });
--- a/browser/components/loop/test/desktop-local/conversation_test.js +++ b/browser/components/loop/test/desktop-local/conversation_test.js @@ -27,16 +27,17 @@ describe("loop.conversation", function() return JSON.stringify({textContent: "fakeText"}); }, get locale() { return "en-US"; }, setLoopCharPref: sandbox.stub(), getLoopCharPref: sandbox.stub(), getLoopBoolPref: sandbox.stub(), + getCallData: sandbox.stub(), startAlerting: function() {}, stopAlerting: function() {}, ensureRegistered: function() {}, get appVersionInfo() { return { version: "42", channel: "test", platform: "test" @@ -107,17 +108,16 @@ describe("loop.conversation", function() var conversation, client; beforeEach(function() { client = new loop.Client(); conversation = new loop.shared.models.ConversationModel({}, { sdk: {}, pendingCallTimeout: 1000, }); - sandbox.stub(client, "requestCallsInfo"); sandbox.spy(conversation, "setIncomingSessionData"); sandbox.stub(conversation, "setOutgoingSessionData"); }); describe("Routes", function() { var router; beforeEach(function() { @@ -152,57 +152,41 @@ describe("loop.conversation", function() it("should start alerting", function() { sandbox.stub(navigator.mozLoop, "startAlerting"); router.incoming("fakeVersion"); sinon.assert.calledOnce(navigator.mozLoop.startAlerting); }); - it("should set the loopVersion on the conversation model", function() { - router.incoming("fakeVersion"); - - expect(conversation.get("loopVersion")).to.equal("fakeVersion"); - }); - - it("should call requestCallsInfo on the client", + it("should call getCallData on navigator.mozLoop", function() { router.incoming(42); - sinon.assert.calledOnce(client.requestCallsInfo); - sinon.assert.calledWith(client.requestCallsInfo, 42); + sinon.assert.calledOnce(navigator.mozLoop.getCallData); + sinon.assert.calledWith(navigator.mozLoop.getCallData, 42); }); - it("should display an error if requestCallsInfo returns an error", - function(){ - sandbox.stub(notifications, "errorL10n"); - client.requestCallsInfo.callsArgWith(1, "failed"); - - router.incoming(42); - - sinon.assert.calledOnce(notifications.errorL10n); - }); - - describe("requestCallsInfo successful", function() { + describe("getCallData successful", function() { var fakeSessionData, resolvePromise, rejectPromise; beforeEach(function() { fakeSessionData = { sessionId: "sessionId", sessionToken: "sessionToken", apiKey: "apiKey", callType: "callType", callId: "Hello", progressURL: "http://progress.example.com", websocketToken: 123 }; sandbox.stub(router, "_setupWebSocketAndCallView"); - client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]); + navigator.mozLoop.getCallData.returns(fakeSessionData); }); it("should store the session data", function() { router.incoming("fakeVersion"); sinon.assert.calledOnce(conversation.setIncomingSessionData); sinon.assert.calledWithExactly(conversation.setIncomingSessionData, fakeSessionData);
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js +++ b/browser/components/loop/test/mochitest/browser_fxa_login.js @@ -2,27 +2,34 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Test FxA logins with Loop. */ "use strict"; -const gFxAOAuthTokenData = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gFxAOAuthTokenData; +const { + LOOP_SESSION_TYPE, + gFxAOAuthTokenData +} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); + const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?"; +const HAWK_TOKEN_LENGTH = 64; add_task(function* setup() { Services.prefs.setCharPref("loop.server", BASE_URL); Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/"); registerCleanupFunction(function* () { info("cleanup time"); yield promiseDeletedOAuthParams(BASE_URL); Services.prefs.clearUserPref("loop.server"); Services.prefs.clearUserPref("services.push.serverURL"); + Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST)); + Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA)); }); }); add_task(function* checkOAuthParams() { let params = { client_id: "client_id", content_uri: BASE_URL + "/content", oauth_uri: BASE_URL + "/oauth", @@ -156,33 +163,51 @@ add_task(function* basicAuthorizationAnd client_id: "client_id", content_uri: BASE_URL + "/content", oauth_uri: BASE_URL + "/oauth", profile_uri: BASE_URL + "/profile", state: "state", }; yield promiseOAuthParamsSetup(BASE_URL, params); + info("registering"); + mockPushHandler.pushUrl = "https://localhost/pushUrl/guest"; + yield MozLoopService.register(mockPushHandler); + let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST); + let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); + ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token"); + + // Normally the same pushUrl would be registered but we change it in the test + // to be able to check for success on the second registration. + mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa"; + let tokenData = yield MozLoopService.logInToFxA(); ise(tokenData.access_token, "code1_access_token", "Check access_token"); ise(tokenData.scope, "profile", "Check scope"); ise(tokenData.token_type, "bearer", "Check token_type"); + + let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL); + ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL"); + prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA); + padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); + ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token"); }); add_task(function* loginWithParams401() { resetFxA(); let params = { client_id: "client_id", content_uri: BASE_URL + "/content", oauth_uri: BASE_URL + "/oauth", profile_uri: BASE_URL + "/profile", state: "state", test_error: "params_401", }; yield promiseOAuthParamsSetup(BASE_URL, params); + yield MozLoopService.register(mockPushHandler); let loginPromise = MozLoopService.logInToFxA(); yield loginPromise.then(tokenData => { ok(false, "Promise should have rejected"); }, error => { ise(error.code, 401, "Check error code"); ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
--- a/browser/components/loop/test/mochitest/head.js +++ b/browser/components/loop/test/mochitest/head.js @@ -107,8 +107,51 @@ function promiseDeletedOAuthParams(baseU createInstance(Ci.nsIXMLHttpRequest); xhr.open("DELETE", baseURL + "/setup_params", true); xhr.addEventListener("load", () => deferred.resolve(xhr)); xhr.addEventListener("error", deferred.reject); xhr.send(); return deferred.promise; } + +/** + * Get the last registration on the test server. + */ +function promiseOAuthGetRegistration(baseURL) { + let deferred = Promise.defer(); + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + xhr.open("GET", baseURL + "/get_registration", true); + xhr.responseType = "json"; + xhr.addEventListener("load", () => deferred.resolve(xhr)); + xhr.addEventListener("error", deferred.reject); + xhr.send(); + + return deferred.promise; +} + +/** + * This is used to fake push registration and notifications for + * MozLoopService tests. There is only one object created per test instance, as + * once registration has taken place, the object cannot currently be changed. + */ +let mockPushHandler = { + // This sets the registration result to be returned when initialize + // is called. By default, it is equivalent to success. + registrationResult: null, + pushUrl: undefined, + + /** + * MozLoopPushHandler API + */ + initialize: function(registerCallback, notificationCallback) { + registerCallback(this.registrationResult, this.pushUrl); + this._notificationCallback = notificationCallback; + }, + + /** + * Test-only API to simplify notifying a push notification result. + */ + notify: function(version) { + this._notificationCallback(version); + } +};
--- a/browser/components/loop/test/mochitest/loop_fxa.sjs +++ b/browser/components/loop/test/mochitest/loop_fxa.sjs @@ -3,37 +3,44 @@ /** * This is a mock server that implements the FxA endpoints on the Loop server. */ "use strict"; const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"]; +const HAWK_TOKEN_LENGTH = 64; Components.utils.import("resource://gre/modules/NetUtil.jsm"); /** * Entry point for HTTP requests. */ function handleRequest(request, response) { // Look at the query string but ignore past the encoded ? when deciding on the handler. switch (request.queryString.replace(/%3F.*/,"")) { - case "/setup_params": + case "/setup_params": // Test-only setup_params(request, response); return; case "/fxa-oauth/params": params(request, response); return; case encodeURIComponent("/oauth/authorization"): oauth_authorization(request, response); return; case "/fxa-oauth/token": token(request, response); return; + case "/registration": + registration(request, response); + return; + case "/get_registration": // Test-only + get_registration(request, response); + return; } response.setStatusLine(request.httpVersion, 404, "Not Found"); } /** * POST /setup_params * DELETE /setup_params * @@ -42,16 +49,17 @@ function handleRequest(request, response * For a POST the X-Params header should contain a JSON object with keys to set for /fxa-oauth/params. * A DELETE request will delete the stored parameters and should be run in a cleanup function to * avoid interfering with subsequen tests. */ function setup_params(request, response) { response.setHeader("Content-Type", "text/plain", false); if (request.method == "DELETE") { setSharedState("/fxa-oauth/params", ""); + setSharedState("/registration", ""); response.write("Params deleted"); return; } let params = JSON.parse(request.getHeader("X-Params")); if (!params) { response.setStatusLine(request.httpVersion, 400, "Bad Request"); return; } @@ -136,8 +144,35 @@ function token(request, response) { let tokenData = { access_token: payload.code + "_access_token", scope: "profile", token_type: "bearer", }; response.setHeader("Content-Type", "application/json; charset=utf-8", false); response.write(JSON.stringify(tokenData, null, 2)); } + +/** + * POST /registration + * + * Mock Loop registration endpoint which simply returns the simplePushURL with + * padding as the hawk session token. + */ +function registration(request, response) { + let body = NetUtil.readInputStreamToString(request.bodyInputStream, + request.bodyInputStream.available()); + let payload = JSON.parse(body); + setSharedState("/registration", body); + let pushURL = payload.simplePushURL; + // Pad the pushURL with "X" to the token length to simulate a token + let padding = new Array(HAWK_TOKEN_LENGTH - pushURL.length).fill("X").join(""); + response.setHeader("hawk-session-token", pushURL + padding, false); +} + +/** + * GET /get_registration + * + * Used for testing purposes to check if registration succeeded by returning the POST body. + */ +function get_registration(request, response) { + response.setHeader("Content-Type", "application/json; charset=utf-8", false); + response.write(getSharedState("/registration")); +}
--- a/browser/components/loop/test/xpcshell/head.js +++ b/browser/components/loop/test/xpcshell/head.js @@ -22,16 +22,23 @@ const kEndPointUrl = "http://example.com const kUAID = "f47ac11b-58ca-4372-9567-0e02b2c3d479"; // Fake loop server var loopServer; // Ensure loop is always enabled for tests Services.prefs.setBoolPref("loop.enabled", true); +function hawkGetCallsRequest() { + let response = {body: JSON.stringify({calls: [{callId: 4444333221, websocketToken: "0deadbeef0"}]})}, + // Call the first non-null then(resolve) function attached to the fakePromise. + fakePromise = {then: (resolve) => {return resolve ? resolve(response) : fakePromise;}, + catch: () => {return fakePromise;}}; + return fakePromise; +} function setupFakeLoopServer() { loopServer = new HttpServer(); loopServer.start(-1); Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl); Services.prefs.setCharPref("loop.server",
--- a/browser/components/loop/test/xpcshell/test_loopservice_dnd.js +++ b/browser/components/loop/test/xpcshell/test_loopservice_dnd.js @@ -1,16 +1,17 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm"); +let openChatOrig = Chat.open; -let openChatOrig = Chat.open; +const loopServiceModule = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); add_test(function test_get_do_not_disturb() { Services.prefs.setBoolPref("loop.do_not_disturb", false); do_check_false(MozLoopService.doNotDisturb); Services.prefs.setBoolPref("loop.do_not_disturb", true); @@ -33,20 +34,25 @@ add_test(function test_do_not_disturb_di MozLoopService.doNotDisturb = false; MozLoopService.register(mockPushHandler).then(() => { let opened = false; Chat.open = function() { opened = true; }; + let savedHawkClient = loopServiceModule.gHawkClient; + loopServiceModule.gHawkClient = {request: hawkGetCallsRequest}; + mockPushHandler.notify(1); do_check_true(opened, "should open a chat window"); + loopServiceModule.gHawkClient = savedHawkClient; + run_next_test(); }); }); add_task(function test_do_not_disturb_enabled_shouldnt_open_chat_window() { MozLoopService.doNotDisturb = true; // We registered in the previous test, so no need to do that on this one.
--- a/browser/components/loop/test/xpcshell/test_loopservice_notification.js +++ b/browser/components/loop/test/xpcshell/test_loopservice_notification.js @@ -2,32 +2,39 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm"); let openChatOrig = Chat.open; +const loopServiceModule = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); + add_test(function test_openChatWindow_on_notification() { Services.prefs.setCharPref("loop.seenToS", "unseen"); MozLoopService.register(mockPushHandler).then(() => { let opened = false; Chat.open = function() { opened = true; }; + let savedHawkClient = loopServiceModule.gHawkClient; + loopServiceModule.gHawkClient = {request: hawkGetCallsRequest}; + mockPushHandler.notify(1); do_check_true(opened, "should open a chat window"); do_check_eq(Services.prefs.getCharPref("loop.seenToS"), "seen", "should set the pref to 'seen'"); + loopServiceModule.gHawkClient = savedHawkClient; + run_next_test(); }); }); function run_test() { setupFakeLoopServer();
--- a/browser/components/preferences/in-content/main.js +++ b/browser/components/preferences/in-content/main.js @@ -55,32 +55,25 @@ var gMainPane = { #endif // set up the "use current page" label-changing listener this._updateUseCurrentButton(); window.addEventListener("focus", this._updateUseCurrentButton.bind(this), false); this.updateBrowserStartupLastSession(); - // Notify observers that the UI is now ready - Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService) - .notifyObservers(window, "main-pane-loaded", null); - #ifdef XP_WIN // Functionality for "Show tabs in taskbar" on Windows 7 and up. - try { let sysInfo = Cc["@mozilla.org/system-info;1"]. getService(Ci.nsIPropertyBag2); let ver = parseFloat(sysInfo.getProperty("version")); let showTabsInTaskbar = document.getElementById("showTabsInTaskbar"); showTabsInTaskbar.hidden = ver < 6.1; } catch (ex) {} - #endif setEventListener("browser.privatebrowsing.autostart", "change", gMainPane.updateBrowserStartupLastSession); setEventListener("browser.download.dir", "change", gMainPane.displayDownloadDirPref); #ifdef HAVE_SHELL_SERVICE setEventListener("setDefaultButton", "command", @@ -89,16 +82,21 @@ var gMainPane = { setEventListener("useCurrent", "command", gMainPane.setHomePageToCurrent); setEventListener("useBookmark", "command", gMainPane.setHomePageToBookmark); setEventListener("restoreDefaultHomePage", "command", gMainPane.restoreDefaultHomePage); setEventListener("chooseFolder", "command", gMainPane.chooseFolder); + + // Notify observers that the UI is now ready + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "main-pane-loaded", null); }, // HOME PAGE /* * Preferences: * * browser.startup.homepage
--- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -114,8 +114,10 @@ browser.jar: content/browser/devtools/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml) content/browser/devtools/spectrum-frame.xhtml (shared/widgets/spectrum-frame.xhtml) content/browser/devtools/spectrum.css (shared/widgets/spectrum.css) content/browser/devtools/cubic-bezier-frame.xhtml (shared/widgets/cubic-bezier-frame.xhtml) content/browser/devtools/cubic-bezier.css (shared/widgets/cubic-bezier.css) content/browser/devtools/eyedropper.xul (eyedropper/eyedropper.xul) content/browser/devtools/eyedropper/crosshairs.css (eyedropper/crosshairs.css) content/browser/devtools/eyedropper/nocursor.css (eyedropper/nocursor.css) + content/browser/devtools/timeline/timeline.xul (timeline/timeline.xul) + content/browser/devtools/timeline/timeline.js (timeline/timeline.js)
--- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -26,46 +26,49 @@ loader.lazyGetter(this, "OptionsPanel", loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel); loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel); loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel); loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel); loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel); loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel); loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/webaudioeditor/panel").WebAudioEditorPanel); loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel").ProfilerPanel); +loader.lazyGetter(this, "TimelinePanel", () => require("devtools/timeline/panel").TimelinePanel); loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel); +loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel); loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel); -loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel); // Strings const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties"; const inspectorProps = "chrome://browser/locale/devtools/inspector.properties"; +const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties"; const debuggerProps = "chrome://browser/locale/devtools/debugger.properties"; const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties"; const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties"; const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties"; const webAudioEditorProps = "chrome://browser/locale/devtools/webaudioeditor.properties"; -const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties"; const profilerProps = "chrome://browser/locale/devtools/profiler.properties"; +const timelineProps = "chrome://browser/locale/devtools/timeline.properties"; const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties"; +const storageProps = "chrome://browser/locale/devtools/storage.properties"; const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties"; -const storageProps = "chrome://browser/locale/devtools/storage.properties"; loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps)); +loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps)); loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps)); loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps)); loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps)); loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps)); loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps)); loader.lazyGetter(this, "webAudioEditorStrings", () => Services.strings.createBundle(webAudioEditorProps)); loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps)); -loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps)); +loader.lazyGetter(this, "timelineStrings", () => Services.strings.createBundle(timelineProps)); loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps)); +loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps)); loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps)); -loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps)); let Tools = {}; exports.Tools = Tools; // Definitions Tools.options = { id: "options", ordinal: 0, @@ -73,19 +76,21 @@ Tools.options = { icon: "chrome://browser/skin/devtools/tool-options.svg", invertIconForLightTheme: true, bgTheme: "theme-body", label: l10n("options.label", toolboxStrings), iconOnly: true, panelLabel: l10n("options.panelLabel", toolboxStrings), tooltip: l10n("optionsButton.tooltip", toolboxStrings), inMenu: false, + isTargetSupported: function(target) { return true; }, + build: function(iframeWindow, toolbox) { return new OptionsPanel(iframeWindow, toolbox); } } Tools.webConsole = { id: "webconsole", key: l10n("cmd.commandkey", webConsoleStrings), @@ -108,16 +113,17 @@ Tools.webConsole = { return toolbox.focusConsoleInput(); panel.focusInput(); }, isTargetSupported: function(target) { return true; }, + build: function(iframeWindow, toolbox) { return new WebConsolePanel(iframeWindow, toolbox); } }; Tools.inspector = { id: "inspector", accesskey: l10n("inspector.accesskey", inspectorStrings), @@ -225,39 +231,43 @@ Tools.canvasDebugger = { ordinal: 6, visibilityswitch: "devtools.canvasdebugger.enabled", icon: "chrome://browser/skin/devtools/tool-styleeditor.svg", invertIconForLightTheme: true, url: "chrome://browser/content/devtools/canvasdebugger.xul", label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings), panelLabel: l10n("ToolboxCanvasDebugger.panelLabel", canvasDebuggerStrings), tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings), + // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox // (bug 1047520). isTargetSupported: function(target) { return !target.isAddon && !target.chrome; }, + build: function (iframeWindow, toolbox) { return new CanvasDebuggerPanel(iframeWindow, toolbox); } }; Tools.webAudioEditor = { id: "webaudioeditor", ordinal: 10, visibilityswitch: "devtools.webaudioeditor.enabled", icon: "chrome://browser/skin/devtools/tool-webaudio.svg", invertIconForLightTheme: true, url: "chrome://browser/content/devtools/webaudioeditor.xul", label: l10n("ToolboxWebAudioEditor1.label", webAudioEditorStrings), panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel", webAudioEditorStrings), tooltip: l10n("ToolboxWebAudioEditor1.tooltip", webAudioEditorStrings), + isTargetSupported: function(target) { return !target.isAddon; }, + build: function(iframeWindow, toolbox) { return new WebAudioEditorPanel(iframeWindow, toolbox); } }; Tools.jsprofiler = { id: "jsprofiler", accesskey: l10n("profiler.accesskey", profilerStrings), @@ -279,21 +289,42 @@ Tools.jsprofiler = { return !target.isAddon && (!target.isApp || target.form.profilerActor); }, build: function (frame, target) { return new ProfilerPanel(frame, target); } }; +Tools.timeline = { + id: "timeline", + ordinal: 8, + visibilityswitch: "devtools.timeline.enabled", + icon: "chrome://browser/skin/devtools/tool-network.svg", + invertIconForLightTheme: true, + url: "chrome://browser/content/devtools/timeline/timeline.xul", + label: l10n("timeline.label", timelineStrings), + panelLabel: l10n("timeline.panelLabel", timelineStrings), + tooltip: l10n("timeline.tooltip", timelineStrings), + + isTargetSupported: function(target) { + return !target.isAddon; + }, + + build: function (iframeWindow, toolbox) { + let panel = new TimelinePanel(iframeWindow, toolbox); + return panel.open(); + } +}; + Tools.netMonitor = { id: "netmonitor", accesskey: l10n("netmonitor.accesskey", netMonitorStrings), key: l10n("netmonitor.commandkey", netMonitorStrings), - ordinal: 8, + ordinal: 9, modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", visibilityswitch: "devtools.netmonitor.enabled", icon: "chrome://browser/skin/devtools/tool-network.svg", invertIconForLightTheme: true, url: "chrome://browser/content/devtools/netmonitor.xul", label: l10n("netmonitor.label", netMonitorStrings), panelLabel: l10n("netmonitor.panelLabel", netMonitorStrings), tooltip: l10n("netmonitor.tooltip", netMonitorStrings), @@ -307,17 +338,17 @@ Tools.netMonitor = { build: function(iframeWindow, toolbox) { return new NetMonitorPanel(iframeWindow, toolbox); } }; Tools.storage = { id: "storage", key: l10n("storage.commandkey", storageStrings), - ordinal: 9, + ordinal: 10, accesskey: l10n("storage.accesskey", storageStrings), modifiers: "shift", visibilityswitch: "devtools.storage.enabled", icon: "chrome://browser/skin/devtools/tool-storage.svg", invertIconForLightTheme: true, url: "chrome://browser/content/devtools/storage.xul", label: l10n("storage.label", storageStrings), menuLabel: l10n("storage.menuLabel", storageStrings), @@ -332,17 +363,17 @@ Tools.storage = { build: function(iframeWindow, toolbox) { return new StoragePanel(iframeWindow, toolbox); } }; Tools.scratchpad = { id: "scratchpad", - ordinal: 10, + ordinal: 11, visibilityswitch: "devtools.scratchpad.enabled", icon: "chrome://browser/skin/devtools/tool-scratchpad.svg", invertIconForLightTheme: true, url: "chrome://browser/content/devtools/scratchpad.xul", label: l10n("scratchpad.label", scratchpadStrings), panelLabel: l10n("scratchpad.panelLabel", scratchpadStrings), tooltip: l10n("scratchpad.tooltip", scratchpadStrings), inMenu: false, @@ -362,16 +393,17 @@ let defaultTools = [ Tools.webConsole, Tools.inspector, Tools.jsdebugger, Tools.styleEditor, Tools.shaderEditor, Tools.canvasDebugger, Tools.webAudioEditor, Tools.jsprofiler, + Tools.timeline, Tools.netMonitor, Tools.storage, Tools.scratchpad ]; exports.defaultTools = defaultTools; for (let definition of defaultTools) {
--- a/browser/devtools/moz.build +++ b/browser/devtools/moz.build @@ -8,30 +8,31 @@ DIRS += [ 'app-manager', 'canvasdebugger', 'commandline', 'debugger', 'eyedropper', 'fontinspector', 'framework', 'inspector', - 'projecteditor', 'layoutview', 'markupview', 'netmonitor', 'profiler', + 'projecteditor', 'responsivedesign', 'scratchpad', 'shadereditor', 'shared', 'sourceeditor', 'storage', 'styleeditor', 'styleinspector', 'tilt', + 'timeline', 'webaudioeditor', 'webconsole', 'webide', ] EXTRA_COMPONENTS += [ 'devtools-clhandler.js', 'devtools-clhandler.manifest',
--- a/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js +++ b/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js @@ -1,9 +1,9 @@ -/* Any copyright is dedicated to the Public Domain. +s/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Tests if the retrieved profiler data samples are correctly filtered and * normalized before passed to consumers. */ const WAIT_TIME = 1000; // ms
--- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -16,17 +16,18 @@ support-files = [browser_cubic-bezier-02.js] [browser_cubic-bezier-03.js] [browser_graphs-01.js] [browser_graphs-02.js] [browser_graphs-03.js] [browser_graphs-04.js] [browser_graphs-05.js] [browser_graphs-06.js] -[browser_graphs-07.js] +[browser_graphs-07a.js] +[browser_graphs-07b.js] [browser_graphs-08.js] [browser_graphs-09.js] [browser_graphs-10a.js] [browser_graphs-10b.js] [browser_graphs-11a.js] [browser_graphs-11b.js] [browser_graphs-12.js] [browser_graphs-13.js]
rename from browser/devtools/shared/test/browser_graphs-07.js rename to browser/devtools/shared/test/browser_graphs-07a.js
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/test/browser_graphs-07b.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests if selections can't be added via clicking, while not allowed. + +const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }]; +let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {}); +let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); +let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); +let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); + +let test = Task.async(function*() { + yield promiseTab("about:blank"); + yield performTest(); + gBrowser.removeCurrentTab(); + finish(); +}); + +function* performTest() { + let [host, win, doc] = yield createHost(); + let graph = new LineGraphWidget(doc.body, "fps"); + yield graph.once("ready"); + + testGraph(graph); + + graph.destroy(); + host.destroy(); +} + +function testGraph(graph) { + graph.setData(TEST_DATA); + graph.selectionEnabled = false; + + info("Attempting to make a selection."); + + dragStart(graph, 300); + is(graph.hasSelection() || graph.hasSelectionInProgress(), false, + "The graph shouldn't have a selection (1)."); + + hover(graph, 400); + is(graph.hasSelection() || graph.hasSelectionInProgress(), false, + "The graph shouldn't have a selection (2)."); + + dragStop(graph, 500); + is(graph.hasSelection() || graph.hasSelectionInProgress(), false, + "The graph shouldn't have a selection (3)."); +} + +// EventUtils just doesn't work! + +function hover(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + graph._onMouseMove({ clientX: x, clientY: y }); +} + +function dragStart(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + graph._onMouseMove({ clientX: x, clientY: y }); + graph._onMouseDown({ clientX: x, clientY: y }); +} + +function dragStop(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + graph._onMouseMove({ clientX: x, clientY: y }); + graph._onMouseUp({ clientX: x, clientY: y }); +}
--- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -5,17 +5,22 @@ const Cu = Components.utils; Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); -this.EXPORTED_SYMBOLS = ["LineGraphWidget", "BarGraphWidget", "CanvasGraphUtils"]; +this.EXPORTED_SYMBOLS = [ + "AbstractCanvasGraph", + "LineGraphWidget", + "BarGraphWidget", + "CanvasGraphUtils" +]; const HTML_NS = "http://www.w3.org/1999/xhtml"; const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml"; // Generic constants. const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00075; @@ -490,16 +495,22 @@ AbstractCanvasGraph.prototype = { * via a click+drag operation. * @return boolean */ hasSelectionInProgress: function() { return this._selection.start != null && this._selection.end == null; }, /** + * Specifies whether or not mouse selection is allowed. + * @type boolean + */ + selectionEnabled: true, + + /** * Sets the selection bounds. * Use `dropCursor` to hide the cursor. * * @param object cursor * The cursor's { x, y } position. */ setCursor: function(cursor) { if (!cursor || cursor.x == null || cursor.y == null) { @@ -950,16 +961,19 @@ AbstractCanvasGraph.prototype = { */ _onMouseDown: function(e) { let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; switch (this._canvas.getAttribute("input")) { case "hovering-background": case "hovering-region": + if (!this.selectionEnabled) { + break; + } this._selection.start = mouseX; this._selection.end = null; this.emit("selecting"); break; case "hovering-selection-start-boundary": this._selectionResizer.margin = "start"; break; @@ -985,16 +999,19 @@ AbstractCanvasGraph.prototype = { */ _onMouseUp: function(e) { let offset = this._getContainerOffset(); let mouseX = (e.clientX - offset.left) * this._pixelRatio; switch (this._canvas.getAttribute("input")) { case "hovering-background": case "hovering-region": + if (!this.selectionEnabled) { + break; + } if (this.getSelectionWidth() < 1) { let region = this.getHoveredRegion(); if (region) { this._selection.start = region.start; this._selection.end = region.end; this.emit("selecting"); } else { this._selection.start = null;
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/moz.build @@ -0,0 +1,13 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES.devtools.timeline += [ + 'panel.js', + 'widgets/global.js', + 'widgets/overview.js', + 'widgets/waterfall.js' +] + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/panel.js @@ -0,0 +1,63 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Cc, Ci, Cu, Cr } = require("chrome"); + +Cu.import("resource://gre/modules/Task.jsm"); + +loader.lazyRequireGetter(this, "promise"); +loader.lazyRequireGetter(this, "EventEmitter", + "devtools/toolkit/event-emitter"); + +loader.lazyRequireGetter(this, "TimelineFront", + "devtools/server/actors/timeline", true); + +function TimelinePanel(iframeWindow, toolbox) { + this.panelWin = iframeWindow; + this._toolbox = toolbox; + + EventEmitter.decorate(this); +}; + +exports.TimelinePanel = TimelinePanel; + +TimelinePanel.prototype = { + /** + * Open is effectively an asynchronous constructor. + * + * @return object + * A promise that is resolved when the timeline completes opening. + */ + open: Task.async(function*() { + // Local debugging needs to make the target remote. + yield this.target.makeRemote(); + + this.panelWin.gToolbox = this._toolbox; + this.panelWin.gTarget = this.target; + this.panelWin.gFront = new TimelineFront(this.target.client, this.target.form); + yield this.panelWin.startupTimeline(); + + this.isReady = true; + this.emit("ready"); + return this; + }), + + // DevToolPanel API + + get target() this._toolbox.target, + + destroy: Task.async(function*() { + // Make sure this panel is not already destroyed. + if (this._destroyed) { + return; + } + + yield this.panelWin.shutdownTimeline(); + this.emit("destroyed"); + this._destroyed = true; + }) +};
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +skip-if = e10s # Bug 1065355 - devtools tests disabled with e10s +subsuite = devtools +support-files = + doc_simple-test.html + head.js + +[browser_timeline_aaa_run_first_leaktest.js] +[browser_timeline_blueprint.js] +[browser_timeline_overview-initial-selection-01.js] +[browser_timeline_overview-initial-selection-02.js] +[browser_timeline_overview-update.js] +[browser_timeline_panels.js] +[browser_timeline_recording.js] +[browser_timeline_waterfall-background.js] +[browser_timeline_waterfall-generic.js] +[browser_timeline_waterfall-styles.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline leaks on initialization and sudden destruction. + * You can also use this initialization format as a template for other tests. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + + ok(target, "Should have a target available."); + ok(debuggee, "Should have a debuggee available."); + ok(panel, "Should have a panel available."); + + ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window."); + ok(panel.panelWin.gTarget, "Should have a target reference on the panel window."); + ok(panel.panelWin.gFront, "Should have a front reference on the panel window."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_blueprint.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline blueprint has a correct structure. + */ + +function test() { + let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global"); + + ok(TIMELINE_BLUEPRINT, + "A timeline blueprint should be available."); + + ok(Object.keys(TIMELINE_BLUEPRINT).length, + "The timeline blueprint has at least one entry."); + + for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) { + ok("group" in value, + "Each entry in the timeline blueprint contains a `group` key."); + ok("fill" in value, + "Each entry in the timeline blueprint contains a `fill` key."); + ok("stroke" in value, + "Each entry in the timeline blueprint contains a `stroke` key."); + ok("label" in value, + "Each entry in the timeline blueprint contains a `label` key."); + } + + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the overview has an initial selection when recording has finished + * and there is data available. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { EVENTS, TimelineView, TimelineController } = panel.panelWin; + let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 10)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + let markers = TimelineController.getMarkers(); + let selection = TimelineView.overview.getSelection(); + + is((selection.start) | 0, + (markers[0].start * TimelineView.overview.dataScaleX) | 0, + "The initial selection start is correct."); + + is((selection.end - selection.start) | 0, + (selectionRatio * TimelineView.overview.width) | 0, + "The initial selection end is correct."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the overview has no initial selection when recording has finished + * and there is no data available. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { EVENTS, TimelineView, TimelineController } = panel.panelWin; + let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + yield TimelineController._stopRecordingAndDiscardData(); + ok(true, "Recording has ended."); + + let markers = TimelineController.getMarkers(); + let selection = TimelineView.overview.getSelection(); + + is(markers.length, 0, + "There are no markers available."); + is(selection.start, null, + "The initial selection start is correct."); + is(selection.end, null, + "The initial selection end is correct."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_overview-update.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the overview graph is continuously updated. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel("about:blank"); + let { EVENTS, TimelineView, TimelineController } = panel.panelWin; + + yield DevToolsUtils.waitForTime(1000); + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + ok("selectionEnabled" in TimelineView.overview, + "The selection should not be enabled for the overview graph (1)."); + is(TimelineView.overview.selectionEnabled, false, + "The selection should not be enabled for the overview graph (2)."); + is(TimelineView.overview.hasSelection(), false, + "The overview graph shouldn't have a selection before recording."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 10)), + "The overview graph was updated a bunch of times."); + + ok("selectionEnabled" in TimelineView.overview, + "The selection should still not be enabled for the overview graph (3)."); + is(TimelineView.overview.selectionEnabled, false, + "The selection should still not be enabled for the overview graph (4)."); + is(TimelineView.overview.hasSelection(), false, + "The overview graph should not have a selection while recording."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + is(TimelineController.getMarkers().length, 0, + "There are no markers available."); + is(TimelineView.overview.selectionEnabled, true, + "The selection should now be enabled for the overview graph."); + is(TimelineView.overview.hasSelection(), false, + "The overview graph should not have a selection after recording."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_panels.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline panels are correctly shown and hidden when + * recording starts and stops. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { $, EVENTS } = panel.panelWin; + + is($("#record-button").hasAttribute("checked"), false, + "The record button should not be checked yet."); + is($("#timeline-pane").selectedPanel, $("#empty-notice"), + "An empty notice is initially displayed instead of the waterfall view."); + + let whenRecStarted = panel.panelWin.once(EVENTS.RECORDING_STARTED); + EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin); + yield whenRecStarted; + + ok(true, "Recording has started."); + + is($("#record-button").getAttribute("checked"), "true", + "The record button should be checked now."); + is($("#timeline-pane").selectedPanel, $("#recording-notice"), + "A recording notice is now displayed instead of the waterfall view."); + + let whenRecEnded = panel.panelWin.once(EVENTS.RECORDING_ENDED); + EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin); + yield whenRecEnded; + + ok(true, "Recording has ended."); + + is($("#record-button").hasAttribute("checked"), false, + "The record button should be unchecked again."); + is($("#timeline-pane").selectedPanel, $("#timeline-waterfall"), + "A waterfall view is now displayed."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_recording.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline can properly start and stop a recording. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { gFront, TimelineController } = panel.panelWin; + + is((yield gFront.isRecording()), false, + "The timeline actor should not be recording when the tool starts."); + is(TimelineController.getMarkers().length, 0, + "There should be no markers available when the tool starts."); + + yield TimelineController.toggleRecording(); + + is((yield gFront.isRecording()), true, + "The timeline actor should be recording now."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available now."); + + ok("startTime" in TimelineController.getMarkers(), + "A `startTime` field was set on the markers array."); + ok("endTime" in TimelineController.getMarkers(), + "An `endTime` field was set on the markers array."); + ok(TimelineController.getMarkers().endTime > + TimelineController.getMarkers().startTime, + "Some time has passed since the recording started."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_waterfall-background.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the waterfall background is a 1px high canvas stretching across + * the container bounds. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 0)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + // Test the waterfall background. + + let parentWidth = $("#timeline-waterfall").getBoundingClientRect().width; + let waterfallWidth = TimelineView.waterfall._waterfallWidth; + let sidebarWidth = 150; // px + is(waterfallWidth, parentWidth - sidebarWidth, + "The waterfall width is correct.") + + ok(TimelineView.waterfall._canvas, + "A canvas should be created after the recording ended."); + ok(TimelineView.waterfall._ctx, + "A 2d context should be created after the recording ended."); + + is(TimelineView.waterfall._canvas.width, waterfallWidth, + "The canvas width is correct."); + is(TimelineView.waterfall._canvas.height, 1, + "The canvas height is correct."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the waterfall is properly built after finishing a recording. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { $, $$, EVENTS, TimelineController } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 0)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + // Test the header container. + + ok($(".timeline-header-container"), + "A header container should have been created."); + + // Test the header sidebar (left). + + ok($(".timeline-header-sidebar"), + "A header sidebar node should have been created."); + ok($(".timeline-header-sidebar > .timeline-header-name"), + "A header name label should have been created inside the sidebar."); + + // Test the header ticks (right). + + ok($(".timeline-header-ticks"), + "A header ticks node should have been created."); + ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0, + "Some header tick labels should have been created inside the tick node."); + + // Test the markers container. + + ok($(".timeline-marker-container"), + "A marker container should have been created."); + + // Test the markers sidebar (left). + + ok($$(".timeline-marker-sidebar").length, + "Some marker sidebar nodes should have been created."); + ok($$(".timeline-marker-sidebar > .timeline-marker-bullet").length, + "Some marker color bullets should have been created inside the sidebar."); + ok($$(".timeline-marker-sidebar > .timeline-marker-name").length, + "Some marker name labels should have been created inside the sidebar."); + + // Test the markers waterfall (right). + + ok($$(".timeline-marker-waterfall").length, + "Some marker waterfall nodes should have been created."); + ok($$(".timeline-marker-waterfall > .timeline-marker-bar").length, + "Some marker color bars should have been created inside the waterfall."); + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the waterfall is properly built after making a selection + * and the child nodes are styled correctly. + */ + +var gRGB_TO_HSL = { + "rgb(193, 132, 214)": "hsl(285,50%,68%)", + "rgb(152, 61, 183)": "hsl(285,50%,48%)", + "rgb(161, 223, 138)": "hsl(104,57%,71%)", + "rgb(96, 201, 58)": "hsl(104,57%,51%)", + "rgb(240, 195, 111)": "hsl(39,82%,69%)", + "rgb(227, 155, 22)": "hsl(39,82%,49%)", +}; + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global"); + let { $, $$, EVENTS, TimelineController } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 0)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + // Test the table sidebars. + + for (let sidebar of [ + ...$$(".timeline-header-sidebar"), + ...$$(".timeline-marker-sidebar") + ]) { + is(sidebar.getAttribute("width"), "150", + "The table's sidebar width is correct."); + } + + // Test the table ticks. + + for (let tick of $$(".timeline-header-tick")) { + ok(tick.getAttribute("value").match(/^\d+ ms$/), + "The table's timeline ticks appear to have correct labels."); + ok(tick.style.transform.match(/^translateX\(.*px\)$/), + "The table's timeline ticks appear to have proper translations."); + } + + // Test the marker bullets. + + for (let bullet of $$(".timeline-marker-bullet")) { + let type = bullet.getAttribute("type"); + + ok(type in TIMELINE_BLUEPRINT, + "The bullet type is present in the timeline blueprint."); + is(gRGB_TO_HSL[bullet.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill, + "The bullet's background color is correct."); + is(gRGB_TO_HSL[bullet.style.borderColor], TIMELINE_BLUEPRINT[type].stroke, + "The bullet's border color is correct."); + } + + // Test the marker bars. + + for (let bar of $$(".timeline-marker-bar")) { + let type = bar.getAttribute("type"); + + ok(type in TIMELINE_BLUEPRINT, + "The bar type is present in the timeline blueprint."); + is(gRGB_TO_HSL[bar.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill, + "The bar's background color is correct."); + is(gRGB_TO_HSL[bar.style.borderColor], TIMELINE_BLUEPRINT[type].stroke, + "The bar's border color is correct."); + + ok(bar.getAttribute("width") > 0, + "The bar appears to have a proper width."); + ok(bar.style.transform.match(/^translateX\(.*px\)$/), + "The bar appears to have proper translations."); + } + + yield teardown(panel); + finish(); +});
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/doc_simple-test.html @@ -0,0 +1,26 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Timeline test page</title> + </head> + + <body> + <script type="text/javascript"> + function test() { + var a = "Hello world!"; + document.body.style.backgroundColor = "rgba(" + + ((Math.random() * 64)|0) + "," + + ((Math.random() * 16)|0) + "," + + ((Math.random() * 16)|0) + ",1)"; + } + + // Prevent this script from being garbage collected. + window.setInterval(test, 1); + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/test/head.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +// Disable logging for all the tests. Both the debugger server and frontend will +// be affected by this pref. +let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +// Enable the tool while testing. +let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled"); +Services.prefs.setBoolPref("devtools.timeline.enabled", true); + +let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); +let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + +let TargetFactory = devtools.TargetFactory; +let Toolbox = devtools.Toolbox; + +const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/"; +const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html"; + +// All tests are asynchronous. +waitForExplicitFinish(); + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled); +}); + +function addTab(url) { + info("Adding tab: " + url); + + let deferred = promise.defer(); + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let linkedBrowser = tab.linkedBrowser; + + linkedBrowser.addEventListener("load", function onLoad() { + linkedBrowser.removeEventListener("load", onLoad, true); + info("Tab added and finished loading: " + url); + deferred.resolve(tab); + }, true); + + return deferred.promise; +} + +function removeTab(tab) { + info("Removing tab."); + + let deferred = promise.defer(); + let tabContainer = gBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + gBrowser.removeTab(tab); + return deferred.promise; +} + +/** + * Spawns a new tab and starts up a toolbox with the timeline panel + * automatically selected. + * + * Must be used within a task. + * + * @param string url + * The location of the new tab to spawn. + * @return object + * A promise resolved once the timeline is initialized, with the + * [target, debuggee, panel] instances. + */ +function* initTimelinePanel(url) { + info("Initializing a timeline pane."); + + let tab = yield addTab(url); + let target = TargetFactory.forTab(tab); + let debuggee = target.window.wrappedJSObject; + + yield target.makeRemote(); + + let toolbox = yield gDevTools.showToolbox(target, "timeline"); + let panel = toolbox.getCurrentPanel(); + return [target, debuggee, panel]; +} + +/** + * Closes a tab and destroys the toolbox holding a timeline panel. + * + * Must be used within a task. + * + * @param object panel + * The timeline panel, created by the toolbox. + * @return object + * A promise resolved once the timeline, toolbox and debuggee tab + * are destroyed. + */ +function* teardown(panel) { + info("Destroying the specified timeline."); + + let tab = panel.target.tab; + yield panel._toolbox.destroy(); + yield removeTab(tab); +} + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return promise.resolve(true); + } + let deferred = promise.defer(); + setTimeout(function() { + waitUntil(predicate).then(() => deferred.resolve(true)); + }, interval); + return deferred.promise; +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/timeline.js @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/devtools/Loader.jsm"); + +devtools.lazyRequireGetter(this, "promise"); +devtools.lazyRequireGetter(this, "EventEmitter", + "devtools/toolkit/event-emitter"); + +devtools.lazyRequireGetter(this, "Overview", + "devtools/timeline/overview", true); +devtools.lazyRequireGetter(this, "Waterfall", + "devtools/timeline/waterfall", true); + +devtools.lazyImporter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +const OVERVIEW_UPDATE_INTERVAL = 200; +const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15; + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When a recording is started or stopped, via the `stopwatch` button. + RECORDING_STARTED: "Timeline:RecordingStarted", + RECORDING_ENDED: "Timeline:RecordingEnded", + + // When the overview graph is populated with new markers. + OVERVIEW_UPDATED: "Timeline:OverviewUpdated", + + // When the waterfall view is populated with new markers. + WATERFALL_UPDATED: "Timeline:WaterfallUpdated" +}; + +/** + * The current target and the timeline front, set by this tool's host. + */ +let gToolbox, gTarget, gFront; + +/** + * Initializes the timeline controller and views. + */ +let startupTimeline = Task.async(function*() { + yield TimelineView.initialize(); + yield TimelineController.initialize(); +}); + +/** + * Destroys the timeline controller and views. + */ +let shutdownTimeline = Task.async(function*() { + yield TimelineView.destroy(); + yield TimelineController.destroy(); + yield gFront.stop(); +}); + +/** + * Functions handling the timeline frontend controller. + */ +let TimelineController = { + /** + * Permanent storage for the markers streamed by the backend. + */ + _markers: [], + + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this._onRecordingTick = this._onRecordingTick.bind(this); + this._onMarkers = this._onMarkers.bind(this); + gFront.on("markers", this._onMarkers); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + gFront.off("markers", this._onMarkers); + }, + + /** + * Gets the accumulated markers in this recording. + * @return array. + */ + getMarkers: function() { + return this._markers; + }, + + /** + * Starts/stops the timeline recording and streaming. + */ + toggleRecording: Task.async(function*() { + let isRecording = yield gFront.isRecording(); + if (isRecording == false) { + yield this._startRecording(); + } else { + yield this._stopRecording(); + } + }), + + /** + * Starts the recording, updating the UI as needed. + */ + _startRecording: function*() { + this._markers = []; + this._markers.startTime = performance.now(); + this._markers.endTime = performance.now(); + this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL); + + TimelineView.handleRecordingStarted(); + yield gFront.start(); + }, + + /** + * Stops the recording, updating the UI as needed. + */ + _stopRecording: function*() { + clearInterval(this._updateId); + + TimelineView.handleMarkersUpdate(this._markers); + TimelineView.handleRecordingEnded(); + yield gFront.stop(); + }, + + /** + * Used in tests. Stops the recording, discarding the accumulated markers and + * updating the UI as needed. + */ + _stopRecordingAndDiscardData: function*() { + this._markers.length = 0; + yield this._stopRecording(); + }, + + /** + * Callback handling the "markers" event on the timeline front. + * + * @param array markers + * A list of new markers collected since the last time this + * function was invoked. + */ + _onMarkers: function(markers) { + Array.prototype.push.apply(this._markers, markers); + }, + + /** + * Callback invoked at a fixed interval while recording. + * Updates the markers store with the current time and the timeline overview. + */ + _onRecordingTick: function() { + this._markers.endTime = performance.now(); + TimelineView.handleMarkersUpdate(this._markers); + } +}; + +/** + * Functions handling the timeline frontend view. + */ +let TimelineView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: Task.async(function*() { + this.overview = new Overview($("#timeline-overview")); + this.waterfall = new Waterfall($("#timeline-waterfall")); + + this._onSelecting = this._onSelecting.bind(this); + this._onRefresh = this._onRefresh.bind(this); + this.overview.on("selecting", this._onSelecting); + this.overview.on("refresh", this._onRefresh); + + yield this.overview.ready(); + yield this.waterfall.recalculateBounds(); + }), + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + this.overview.off("selecting", this._onSelecting); + this.overview.off("refresh", this._onRefresh); + this.overview.destroy(); + }, + + /** + * Signals that a recording session has started and triggers the appropriate + * changes in the UI. + */ + handleRecordingStarted: function() { + $("#record-button").setAttribute("checked", "true"); + $("#timeline-pane").selectedPanel = $("#recording-notice"); + + this.overview.selectionEnabled = false; + this.overview.dropSelection(); + this.overview.setData([]); + this.waterfall.clearView(); + + window.emit(EVENTS.RECORDING_STARTED); + }, + + /** + * Signals that a recording session has ended and triggers the appropriate + * changes in the UI. + */ + handleRecordingEnded: function() { + $("#record-button").removeAttribute("checked"); + $("#timeline-pane").selectedPanel = $("#timeline-waterfall"); + + this.overview.selectionEnabled = true; + + let markers = TimelineController.getMarkers(); + if (markers.length) { + let start = markers[0].start * this.overview.dataScaleX; + let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO; + this.overview.setSelection({ start, end }); + } else { + let duration = markers.endTime - markers.startTime; + this.waterfall.setData(markers, 0, duration); + } + + window.emit(EVENTS.RECORDING_ENDED); + }, + + /** + * Signals that a new set of markers was made available by the controller, + * or that the overview graph needs to be updated. + * + * @param array markers + * A list of new markers collected since the recording has started. + */ + handleMarkersUpdate: function(markers) { + this.overview.setData(markers); + window.emit(EVENTS.OVERVIEW_UPDATED); + }, + + /** + * Callback handling the "selecting" event on the timeline overview. + */ + _onSelecting: function() { + if (!this.overview.hasSelection() && + !this.overview.hasSelectionInProgress()) { + this.waterfall.clearView(); + return; + } + let selection = this.overview.getSelection(); + let start = selection.start / this.overview.dataScaleX; + let end = selection.end / this.overview.dataScaleX; + + let markers = TimelineController.getMarkers(); + let timeStart = Math.min(start, end); + let timeEnd = Math.max(start, end); + this.waterfall.setData(markers, timeStart, timeEnd); + }, + + /** + * Callback handling the "refresh" event on the timeline overview. + */ + _onRefresh: function() { + this.waterfall.recalculateBounds(); + this._onSelecting(); + } +}; + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helpers. + */ +function $(selector, target = document) { + return target.querySelector(selector); +} +function $$(selector, target = document) { + return target.querySelectorAll(selector); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/timeline.xul @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/timeline.css" type="text/css"?> +<!DOCTYPE window [ + <!ENTITY % timelineDTD SYSTEM "chrome://browser/locale/devtools/timeline.dtd"> + %timelineDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://browser/content/devtools/theme-switching.js"/> + <script type="application/javascript" src="timeline.js"/> + + <vbox class="theme-body" flex="1"> + <toolbar id="timeline-toolbar" + class="devtools-toolbar"> + <hbox id="recordings-controls" + class="devtools-toolbarbutton-group" + align="center"> + <toolbarbutton id="record-button" + class="devtools-toolbarbutton" + oncommand="TimelineController.toggleRecording()" + tooltiptext="&timelineUI.recordButton.tooltip;"/> + <spacer flex="1"/> + <label id="record-label" + value="&timelineUI.recordLabel;"/> + </hbox> + </toolbar> + + <vbox id="timeline-overview"/> + + <deck id="timeline-pane" + flex="1"> + <hbox id="empty-notice" + class="notice-container" + align="center" + pack="center" + flex="1"> + <label value="&timelineUI.emptyNotice1;"/> + <button id="profiling-notice-button" + class="devtools-toolbarbutton" + standalone="true" + oncommand="TimelineController.toggleRecording()"/> + <label value="&timelineUI.emptyNotice2;"/> + </hbox> + + <hbox id="recording-notice" + class="notice-container" + align="center" + pack="center" + flex="1"> + <label value="&timelineUI.stopNotice1;"/> + <button id="profiling-notice-button" + class="devtools-toolbarbutton" + standalone="true" + checked="true" + oncommand="TimelineController.toggleRecording()"/> + <label value="&timelineUI.stopNotice2;"/> + </hbox> + + <vbox id="timeline-waterfall" flex="1"/> + </deck> + </vbox> +</window>
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/widgets/global.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {Cc, Ci, Cu, Cr} = require("chrome"); + +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); + +/** + * Localization convenience methods. + */ +const STRINGS_URI = "chrome://browser/locale/devtools/timeline.properties"; +const L10N = new ViewHelpers.L10N(STRINGS_URI); + +/** + * A simple schema for mapping markers to the timeline UI. The keys correspond + * to marker names, while the values are objects with the following format: + * - group: the row index in the timeline overview graph; multiple markers + * can be added on the same row. @see <overview.js/buildGraphImage> + * - fill: a fill color used when drawing the marker + * - stroke: a stroke color used when drawing the marker + * - label: the label used in the waterfall to identify the marker + * + * Whenever this is changed, browser_timeline_waterfall-styles.js *must* be + * updated as well. + */ +const TIMELINE_BLUEPRINT = { + "Styles": { + group: 0, + fill: "hsl(285,50%,68%)", + stroke: "hsl(285,50%,48%)", + label: L10N.getStr("timeline.label.styles") + }, + "Reflow": { + group: 2, + fill: "hsl(104,57%,71%)", + stroke: "hsl(104,57%,51%)", + label: L10N.getStr("timeline.label.reflow") + }, + "Paint": { + group: 1, + fill: "hsl(39,82%,69%)", + stroke: "hsl(39,82%,49%)", + label: L10N.getStr("timeline.label.paint") + } +}; + +// Exported symbols. +exports.L10N = L10N; +exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/widgets/overview.js @@ -0,0 +1,208 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * This file contains the "overview" graph, which is a minimap of all the + * timeline data. Regions inside it may be selected, determining which markers + * are visible in the "waterfall". + */ + +const {Cc, Ci, Cu, Cr} = require("chrome"); + +Cu.import("resource:///modules/devtools/Graphs.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); + +loader.lazyRequireGetter(this, "L10N", + "devtools/timeline/global", true); +loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", + "devtools/timeline/global", true); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const OVERVIEW_HEADER_HEIGHT = 20; // px +const OVERVIEW_BODY_HEIGHT = 50; // px + +const OVERVIEW_BACKGROUND_COLOR = "#fff"; +const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666"; +const OVERVIEW_SELECTION_LINE_COLOR = "#555"; +const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)"; +const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)"; + +const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms +const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px +const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px +const OVERVIEW_HEADER_BACKGROUND = "#ebeced"; +const OVERVIEW_HEADER_TEXT_COLOR = "#18191a"; +const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px +const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif"; +const OVERVIEW_HEADER_TEXT_PADDING = 6; // px +const OVERVIEW_TIMELINE_STROKES = "#aaa"; +const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1]; +const OVERVIEW_MARKER_DURATION_MIN = 4; // ms +const OVERVIEW_GROUP_VERTICAL_PADDING = 6; // px +const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)"; + +/** + * An overview for the timeline data. + * + * @param nsIDOMNode parent + * The parent node holding the overview. + */ +function Overview(parent, ...args) { + AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]); + this.once("ready", () => { + this.setBlueprint(TIMELINE_BLUEPRINT); + + var preview = []; + preview.startTime = 0; + preview.endTime = 1000; + this.setData(preview); + }); +} + +Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, { + fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT, + clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR, + selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR, + selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR, + selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR, + + /** + * List of names and colors used to paint this overview. + * @see TIMELINE_BLUEPRINT in timeline/widgets/global.js + */ + setBlueprint: function(blueprint) { + this._paintBatches = new Map(); + this._lastGroup = 0; + + for (let type in blueprint) { + this._paintBatches.set(type, { style: blueprint[type], batch: [] }); + this._lastGroup = Math.max(this._lastGroup, blueprint[type].group); + } + }, + + /** + * Renders the graph's data source. + * @see AbstractCanvasGraph.prototype.buildGraphImage + */ + buildGraphImage: function() { + let { canvas, ctx } = this._getNamedCanvas("overview-data"); + let canvasWidth = this._width; + let canvasHeight = this._height; + let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio; + let availableWidth = canvasWidth - safeBounds; + + // Group markers into separate paint batches. This is necessary to + // draw all markers sharing the same style at once. + + for (let marker of this._data) { + this._paintBatches.get(marker.name).batch.push(marker); + } + + // Calculate each group's height, and the time-based scaling. + + let totalGroups = this._lastGroup + 1; + let headerHeight = OVERVIEW_HEADER_HEIGHT * this._pixelRatio; + let groupHeight = OVERVIEW_BODY_HEIGHT * this._pixelRatio / totalGroups; + let groupPadding = OVERVIEW_GROUP_VERTICAL_PADDING * this._pixelRatio; + + let totalTime = (this._data.endTime - this._data.startTime) || 0; + let dataScale = this.dataScaleX = availableWidth / totalTime; + + // Draw the header and overview background. + + ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND; + ctx.fillRect(0, 0, canvasWidth, headerHeight); + + ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR; + ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight); + + // Draw the alternating odd/even group backgrounds. + + ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND; + ctx.beginPath(); + + for (let i = 1; i < totalGroups; i += 2) { + let top = headerHeight + i * groupHeight; + ctx.rect(0, top, canvasWidth, groupHeight); + } + + ctx.fill(); + + // Draw the timeline header ticks. + + ctx.textBaseline = "middle"; + let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio; + let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY; + ctx.font = fontSize + "px " + fontFamily; + ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR; + ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES; + ctx.beginPath(); + + let tickInterval = this._findOptimalTickInterval(dataScale); + let headerTextPadding = OVERVIEW_HEADER_TEXT_PADDING * this._pixelRatio; + + for (let x = 0; x < availableWidth; x += tickInterval) { + let left = x + headerTextPadding; + let time = Math.round(x / dataScale); + let label = L10N.getFormatStr("timeline.tick", time); + ctx.fillText(label, left, headerHeight / 2 + 1); + ctx.moveTo(x, 0); + ctx.lineTo(x, canvasHeight); + } + + ctx.stroke(); + + // Draw the timeline markers. + + for (let [, { style, batch }] of this._paintBatches) { + let top = headerHeight + style.group * groupHeight + groupPadding / 2; + let height = groupHeight - groupPadding; + + let gradient = ctx.createLinearGradient(0, top, 0, top + height); + gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[0], style.stroke); + gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[1], style.fill); + gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[2], style.fill); + gradient.addColorStop(OVERVIEW_MARKERS_COLOR_STOPS[3], style.stroke); + ctx.fillStyle = gradient; + ctx.beginPath(); + + for (let { start, end } of batch) { + let left = start * dataScale; + let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN); + let width = Math.max(duration * dataScale, this._pixelRatio); + ctx.rect(left, top, width, height); + } + + ctx.fill(); + + // Since all the markers in this batch (thus sharing the same style) have + // been drawn, empty it. The next time new markers will be available, + // they will be sorted and drawn again. + batch.length = 0; + } + + return canvas; + }, + + /** + * Finds the optimal tick interval between time markers in this overview. + */ + _findOptimalTickInterval: function(dataScale) { + let timingStep = OVERVIEW_HEADER_TICKS_MULTIPLE; + let spacingMin = OVERVIEW_HEADER_TICKS_SPACING_MIN * this._pixelRatio; + + while (true) { + let scaledStep = dataScale * timingStep; + if (scaledStep < spacingMin) { + timingStep <<= 1; + continue; + } + return scaledStep; + } + } +}); + +exports.Overview = Overview;
new file mode 100644 --- /dev/null +++ b/browser/devtools/timeline/widgets/waterfall.js @@ -0,0 +1,444 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * This file contains the "waterfall" view, essentially a detailed list + * of all the markers in the timeline data. + */ + +const {Cc, Ci, Cu, Cr} = require("chrome"); + +loader.lazyRequireGetter(this, "L10N", + "devtools/timeline/global", true); +loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT", + "devtools/timeline/global", true); + +loader.lazyImporter(this, "setNamedTimeout", + "resource:///modules/devtools/ViewHelpers.jsm"); +loader.lazyImporter(this, "clearNamedTimeout", + "resource:///modules/devtools/ViewHelpers.jsm"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT = 30; +const TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms + +const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms +const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px +const TIMELINE_HEADER_TEXT_PADDING = 3; // px + +const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px +const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px + +const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms +const WATERFALL_BACKGROUND_TICKS_SCALES = 3; +const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px +const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144]; +const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte +const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte + +/** + * A detailed waterfall view for the timeline data. + * + * @param nsIDOMNode parent + * The parent node holding the waterfall. + */ +function Waterfall(parent) { + this._parent = parent; + this._document = parent.ownerDocument; + this._fragment = this._document.createDocumentFragment(); + this._outstandingMarkers = []; + + this._headerContents = this._document.createElement("hbox"); + this._headerContents.className = "timeline-header-contents"; + this._parent.appendChild(this._headerContents); + + this._listContents = this._document.createElement("vbox"); + this._listContents.className = "timeline-list-contents"; + this._listContents.setAttribute("flex", "1"); + this._parent.appendChild(this._listContents); + + this._isRTL = this._getRTL(); + + // Lazy require is a bit slow, and these are hot objects. + this._l10n = L10N; + this._blueprint = TIMELINE_BLUEPRINT; + this._setNamedTimeout = setNamedTimeout; + this._clearNamedTimeout = clearNamedTimeout; +} + +Waterfall.prototype = { + /** + * Populates this view with the provided data source. + * + * @param array markers + * A list of markers received from the controller. + * @param number timeStart + * The delta time (in milliseconds) to start drawing from. + * @param number timeEnd + * The delta time (in milliseconds) to end drawing at. + */ + setData: function(markers, timeStart, timeEnd) { + this.clearView(); + + let dataScale = this._waterfallWidth / (timeEnd - timeStart); + this._drawWaterfallBackground(dataScale); + this._buildHeader(this._headerContents, timeStart, dataScale); + this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale); + }, + + /** + * Depopulates this view. + */ + clearView: function() { + while (this._headerContents.hasChildNodes()) { + this._headerContents.firstChild.remove(); + } + while (this._listContents.hasChildNodes()) { + this._listContents.firstChild.remove(); + } + this._listContents.scrollTop = 0; + this._outstandingMarkers.length = 0; + this._clearNamedTimeout("flush-outstanding-markers"); + }, + + /** + * Calculates and stores the available width for the waterfall. + * This should be invoked every time the container window is resized. + */ + recalculateBounds: function() { + let bounds = this._parent.getBoundingClientRect(); + this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH; + }, + + /** + * Creates the header part of this view. + * + * @param nsIDOMNode parent + * The parent node holding the header. + * @param number timeStart + * @see Waterfall.prototype.setData + * @param number dataScale + * The time scale of the data source. + */ + _buildHeader: function(parent, timeStart, dataScale) { + let container = this._document.createElement("hbox"); + container.className = "timeline-header-container"; + container.setAttribute("flex", "1"); + + let sidebar = this._document.createElement("hbox"); + sidebar.className = "timeline-header-sidebar theme-sidebar"; + sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH); + sidebar.setAttribute("align", "center"); + container.appendChild(sidebar); + + let name = this._document.createElement("label"); + name.className = "plain timeline-header-name"; + name.setAttribute("value", this._l10n.getStr("timeline.records")); + sidebar.appendChild(name); + + let ticks = this._document.createElement("hbox"); + ticks.className = "timeline-header-ticks"; + ticks.setAttribute("align", "center"); + ticks.setAttribute("flex", "1"); + container.appendChild(ticks); + + let offset = this._isRTL ? this._waterfallWidth : 0; + let direction = this._isRTL ? -1 : 1; + let tickInterval = this._findOptimalTickInterval({ + ticksMultiple: TIMELINE_HEADER_TICKS_MULTIPLE, + ticksSpacingMin: TIMELINE_HEADER_TICKS_SPACING_MIN, + dataScale: dataScale + }); + + for (let x = 0; x < this._waterfallWidth; x += tickInterval) { + let start = x + direction * TIMELINE_HEADER_TEXT_PADDING; + let time = Math.round(timeStart + x / dataScale); + let label = this._l10n.getFormatStr("timeline.tick", time); + + let node = this._document.createElement("label"); + node.className = "plain timeline-header-tick"; + node.style.transform = "translateX(" + (start - offset) + "px)"; + node.setAttribute("value", label); + ticks.appendChild(node); + } + + parent.appendChild(container); + }, + + /** + * Creates the markers part of this view. + * + * @param nsIDOMNode parent + * The parent node holding the markers. + * @param number timeStart + * @see Waterfall.prototype.setData + * @param number dataScale + * The time scale of the data source. + */ + _buildMarkers: function(parent, markers, timeStart, timeEnd, dataScale) { + let processed = 0; + + for (let marker of markers) { + if (!isMarkerInRange(marker, timeStart, timeEnd)) { + continue; + } + // Only build and display a finite number of markers initially, to + // preserve a snappy UI. After a certain delay, continue building the + // outstanding markers while there's (hopefully) no user interaction. + let arguments_ = [this._fragment, marker, timeStart, dataScale]; + if (processed++ < TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT) { + this._buildMarker.apply(this, arguments_); + } else { + this._outstandingMarkers.push(arguments_); + } + } + + // If there are no outstanding markers, add a dummy "spacer" at the end + // to fill up any remaining available space in the UI. + if (!this._outstandingMarkers.length) { + this._buildMarker(this._fragment, null); + } + // Otherwise prepare flushing the outstanding markers after a small delay. + else { + this._setNamedTimeout("flush-outstanding-markers", + TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY, + () => this._buildOutstandingMarkers(parent)); + } + + parent.appendChild(this._fragment); + }, + + /** + * Finishes building the outstanding markers in this view. + * @see Waterfall.prototype._buildMarkers + */ + _buildOutstandingMarkers: function(parent) { + if (!this._outstandingMarkers.length) { + return; + } + for (let args of this._outstandingMarkers) { + this._buildMarker.apply(this, args); + } + this._outstandingMarkers.length = 0; + parent.appendChild(this._fragment); + }, + + /** + * Creates a single marker in this view. + * + * @param nsIDOMNode parent + * The parent node holding the marker. + * @param object marker + * The { name, start, end } marker in the data source. + * @param timeStart + * @see Waterfall.prototype.setData + * @param number dataScale + * @see Waterfall.prototype._buildMarkers + */ + _buildMarker: function(parent, marker, timeStart, dataScale) { + let container = this._document.createElement("hbox"); + container.className = "timeline-marker-container"; + + if (marker) { + this._buildMarkerSidebar(container, marker); + this._buildMarkerWaterfall(container, marker, timeStart, dataScale); + } else { + this._buildMarkerSpacer(container); + container.setAttribute("flex", "1"); + container.setAttribute("is-spacer", ""); + } + + parent.appendChild(container); + }, + + /** + * Creates the sidebar part of a marker in this view. + * + * @param nsIDOMNode container + * The container node representing the marker in this view. + * @param object marker + * @see Waterfall.prototype._buildMarker + */ + _buildMarkerSidebar: function(container, marker) { + let blueprint = this._blueprint[marker.name]; + + let sidebar = this._document.createElement("hbox"); + sidebar.className = "timeline-marker-sidebar theme-sidebar"; + sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH); + sidebar.setAttribute("align", "center"); + + let bullet = this._document.createElement("hbox"); + bullet.className = "timeline-marker-bullet"; + bullet.style.backgroundColor = blueprint.fill; + bullet.style.borderColor = blueprint.stroke; + bullet.setAttribute("type", marker.name); + sidebar.appendChild(bullet); + + let name = this._document.createElement("label"); + name.className = "plain timeline-marker-name"; + name.setAttribute("value", blueprint.label); + sidebar.appendChild(name); + + container.appendChild(sidebar); + }, + + /** + * Creates the waterfall part of a marker in this view. + * + * @param nsIDOMNode container + * The container node representing the marker. + * @param object marker + * @see Waterfall.prototype._buildMarker + * @param timeStart + * @see Waterfall.prototype.setData + * @param number dataScale + * @see Waterfall.prototype._buildMarkers + */ + _buildMarkerWaterfall: function(container, marker, timeStart, dataScale) { + let blueprint = this._blueprint[marker.name]; + + let waterfall = this._document.createElement("hbox"); + waterfall.className = "timeline-marker-waterfall"; + waterfall.setAttribute("flex", "1"); + + let start = (marker.start - timeStart) * dataScale; + let width = (marker.end - marker.start) * dataScale; + let offset = this._isRTL ? this._waterfallWidth : 0; + + let bar = this._document.createElement("hbox"); + bar.className = "timeline-marker-bar"; + bar.style.backgroundColor = blueprint.fill; + bar.style.borderColor = blueprint.stroke; + bar.style.transform = "translateX(" + (start - offset) + "px)"; + bar.setAttribute("type", marker.name); + bar.setAttribute("width", Math.max(width, TIMELINE_MARKER_BAR_WIDTH_MIN)); + waterfall.appendChild(bar); + + container.appendChild(waterfall); + }, + + /** + * Creates a dummy spacer as an empty marker. + * + * @param nsIDOMNode container + * The container node representing the marker. + */ + _buildMarkerSpacer: function(container) { + let sidebarSpacer = this._document.createElement("spacer"); + sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar"; + sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH); + + let waterfallSpacer = this._document.createElement("spacer"); + waterfallSpacer.className = "timeline-marker-waterfall"; + waterfallSpacer.setAttribute("flex", "1"); + + container.appendChild(sidebarSpacer); + container.appendChild(waterfallSpacer); + }, + + /** + * Creates the background displayed on the marker's waterfall. + * + * @param number dataScale + * @see Waterfall.prototype._buildMarkers + */ + _drawWaterfallBackground: function(dataScale) { + if (!this._canvas || !this._ctx) { + this._canvas = this._document.createElementNS(HTML_NS, "canvas"); + this._ctx = this._canvas.getContext("2d"); + } + let canvas = this._canvas; + let ctx = this._ctx; + + // Nuke the context. + let canvasWidth = canvas.width = this._waterfallWidth; + let canvasHeight = canvas.height = 1; // Awww yeah, 1px, repeats on Y axis. + + // Start over. + let imageData = ctx.createImageData(canvasWidth, canvasHeight); + let pixelArray = imageData.data; + + let buf = new ArrayBuffer(pixelArray.length); + let view8bit = new Uint8ClampedArray(buf); + let view32bit = new Uint32Array(buf); + + // Build new millisecond tick lines... + let [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB; + let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN; + let tickInterval = this._findOptimalTickInterval({ + ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE, + ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN, + dataScale: dataScale + }); + + // Insert one pixel for each division on each scale. + for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) { + let increment = tickInterval * Math.pow(2, i); + for (let x = 0; x < canvasWidth; x += increment) { + let position = x | 0; + view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r; + } + alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD; + } + + // Flush the image data and cache the waterfall background. + pixelArray.set(view8bit); + ctx.putImageData(imageData, 0, 0); + this._document.mozSetImageElement("waterfall-background", canvas); + }, + + /** + * Finds the optimal tick interval between time markers in this timeline. + * + * @param number ticksMultiple + * @param number ticksSpacingMin + * @param number dataScale + * @return number + */ + _findOptimalTickInterval: function({ ticksMultiple, ticksSpacingMin, dataScale }) { + let timingStep = ticksMultiple; + + while (true) { + let scaledStep = dataScale * timingStep; + if (scaledStep < ticksSpacingMin) { + timingStep <<= 1; + continue; + } + return scaledStep; + } + }, + + /** + * Returns true if this is document is in RTL mode. + * @return boolean + */ + _getRTL: function() { + let win = this._document.defaultView; + let doc = this._document.documentElement; + return win.getComputedStyle(doc, null).direction == "rtl"; + } +}; + +/** + * Checks if a given marker is in the specified time range. + * + * @param object e + * The marker containing the { start, end } timestamps. + * @param number start + * The earliest allowed time. + * @param number end + * The latest allowed time. + * @return boolean + * True if the marker fits inside the specified time range. + */ +function isMarkerInRange(e, start, end) { + return (e.start >= start && e.end <= end) || // bounds inside + (e.start < start && e.end > end) || // bounds outside + (e.start < start && e.end >= start && e.end <= end) || // overlap start + (e.end > end && e.start >= start && e.start <= end); // overlap end +} + +exports.Waterfall = Waterfall;
new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/devtools/timeline.dtd @@ -0,0 +1,30 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!-- LOCALIZATION NOTE : FILE This file contains the Timeline strings --> +<!-- LOCALIZATION NOTE : FILE Do not translate commandkey --> + +<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to + - keep it in English, or another language commonly spoken among web developers. + - You want to make that choice consistent across the developer tools. + - A good criteria is the language in which you'd find the best + - documentation on web development on the web. --> + +<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed + - on a button that starts a new recording. --> +<!ENTITY timelineUI.recordButton.tooltip "Record timeline operations"> + +<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed + - as a label to signal that a recording is in progress. --> +<!ENTITY timelineUI.recordLabel "Recording…"> + +<!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown + - in the timeline view when empty. --> +<!ENTITY timelineUI.emptyNotice1 "Click on the"> +<!ENTITY timelineUI.emptyNotice2 "button to start recording timeline events."> + +<!-- LOCALIZATION NOTE (timelineUI.stopNotice1/2): This is the label shown + - in the timeline view while recording. --> +<!ENTITY timelineUI.stopNotice1 "Click on the"> +<!ENTITY timelineUI.stopNotice2 "button again to stop recording.">
new file mode 100644 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/devtools/timeline.properties @@ -0,0 +1,40 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# LOCALIZATION NOTE These strings are used inside the Timeline +# which is available from the Web Developer sub-menu -> 'Timeline'. +# The correct localization of this file might be to keep it in +# English, or another language commonly spoken among web developers. +# You want to make that choice consistent across the developer tools. +# A good criteria is the language in which you'd find the best +# documentation on web development on the web. + +# LOCALIZATION NOTE (timeline.label): +# This string is displayed in the title of the tab when the timeline is +# displayed inside the developer tools window and in the Developer Tools Menu. +timeline.label=Timeline + +# LOCALIZATION NOTE (timeline.panelLabel): +# This is used as the label for the toolbox panel. +timeline.panelLabel=Timeline Panel + +# LOCALIZATION NOTE (timeline.tooltip): +# This string is displayed in the tooltip of the tab when the timeline is +# displayed inside the developer tools window. +timeline.tooltip=Performance Timeline + +# LOCALIZATION NOTE (timeline.tick): +# This string is displayed in the timeline overview, for delimiting ticks +# by time, in milliseconds. +timeline.tick=%S ms + +# LOCALIZATION NOTE (timeline.records): +# This string is displayed in the timeline waterfall, as a title for the menu. +timeline.records=RECORDS + +# LOCALIZATION NOTE (timeline.label.*): +# These strings are displayed in the timeline waterfall, identifying markers. +timeline.label.styles=Styles +timeline.label.reflow=Reflow +timeline.label.paint=Paint
--- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -53,16 +53,18 @@ locale/browser/devtools/sourceeditor.dtd (%chrome/browser/devtools/sourceeditor.dtd) locale/browser/devtools/profiler.dtd (%chrome/browser/devtools/profiler.dtd) locale/browser/devtools/profiler.properties (%chrome/browser/devtools/profiler.properties) locale/browser/devtools/layoutview.dtd (%chrome/browser/devtools/layoutview.dtd) locale/browser/devtools/responsiveUI.properties (%chrome/browser/devtools/responsiveUI.properties) locale/browser/devtools/toolbox.dtd (%chrome/browser/devtools/toolbox.dtd) locale/browser/devtools/toolbox.properties (%chrome/browser/devtools/toolbox.properties) locale/browser/devtools/inspector.dtd (%chrome/browser/devtools/inspector.dtd) + locale/browser/devtools/timeline.dtd (%chrome/browser/devtools/timeline.dtd) + locale/browser/devtools/timeline.properties (%chrome/browser/devtools/timeline.properties) locale/browser/devtools/projecteditor.properties (%chrome/browser/devtools/projecteditor.properties) locale/browser/devtools/eyedropper.properties (%chrome/browser/devtools/eyedropper.properties) locale/browser/devtools/connection-screen.dtd (%chrome/browser/devtools/connection-screen.dtd) locale/browser/devtools/connection-screen.properties (%chrome/browser/devtools/connection-screen.properties) locale/browser/devtools/font-inspector.dtd (%chrome/browser/devtools/font-inspector.dtd) locale/browser/devtools/app-manager.dtd (%chrome/browser/devtools/app-manager.dtd) locale/browser/devtools/app-manager.properties (%chrome/browser/devtools/app-manager.properties) locale/browser/devtools/webide.dtd (%chrome/browser/devtools/webide.dtd)
new file mode 100644 --- /dev/null +++ b/browser/themes/linux/devtools/timeline.css @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +%include ../../shared/devtools/timeline.inc.css
--- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -242,16 +242,17 @@ browser.jar: skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png) skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png) skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png) * skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css) * skin/classic/browser/devtools/debugger.css (devtools/debugger.css) skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css) * skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css) * skin/classic/browser/devtools/profiler.css (devtools/profiler.css) +* skin/classic/browser/devtools/timeline.css (devtools/timeline.css) * skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css) * skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css) * skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css) skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css) skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css) * skin/classic/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css) skin/classic/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png) skin/classic/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png)
new file mode 100644 --- /dev/null +++ b/browser/themes/osx/devtools/timeline.css @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +%include ../shared.inc +%include ../../shared/devtools/timeline.inc.css
--- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -369,16 +369,17 @@ browser.jar: skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png) skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png) skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png) * skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css) * skin/classic/browser/devtools/debugger.css (devtools/debugger.css) skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css) * skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css) * skin/classic/browser/devtools/profiler.css (devtools/profiler.css) +* skin/classic/browser/devtools/timeline.css (devtools/timeline.css) * skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css) * skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css) * skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css) skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css) skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css) * skin/classic/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css) skin/classic/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png) skin/classic/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png)
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css +++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css @@ -886,17 +886,23 @@ toolbarbutton[panel-multiview-anchor="tr position: absolute; top: 0; height: 100%; width: @exitSubviewGutterWidth@; background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted.png), linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0)); background-repeat: no-repeat; background-color: Highlight; - background-position: left 10px center, 0; /* this doesn't need to be changed for RTL */ + background-position: left 10px center, 0; +} + +#PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after { + background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl.png), + linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0)); + background-position: right 10px center, 0; } toolbarbutton[panel-multiview-anchor="true"] { background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted.png), linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0)); background-position: right calc(@menuPanelButtonWidth@ / 2 - @exitSubviewGutterWidth@ + 2px) center; background-repeat: no-repeat, repeat; }
new file mode 100644 --- /dev/null +++ b/browser/themes/shared/devtools/timeline.inc.css @@ -0,0 +1,159 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#record-button { + list-style-image: url(profiler-stopwatch.svg); +} + +#record-button[checked] { + list-style-image: url(profiler-stopwatch-checked.svg); +} + +#record-button:not([checked]) ~ #record-label { + visibility: hidden; +} + +.notice-container { + font-size: 120%; + padding-bottom: 35vh; +} + +.theme-dark .notice-container { + background: #343c45; /* Toolbars */ + color: #f5f7fa; /* Light foreground text */ +} + +.theme-light .notice-container { + background: #f0f1f2; /* Toolbars */ + color: #585959; /* Grey foreground text */ +} + +#empty-notice button, +#recording-notice button { + min-width: 30px; + min-height: 28px; + margin: 0; + list-style-image: url(profiler-stopwatch.svg); +} + +#empty-notice button[checked], +#recording-notice button[checked] { + list-style-image: url(profiler-stopwatch-checked.svg); +} + +#empty-notice button .button-text, +#recording-notice button .button-text { + display: none; +} + +.theme-dark #timeline-overview { + border-bottom: 1px solid #000; +} + +.theme-light #timeline-overview { + border-bottom: 1px solid #aaa; +} + +.timeline-list-contents { + /* Hack: force hardware acceleration */ + transform: translateZ(1px); + overflow-x: hidden; + overflow-y: auto; +} + +.timeline-header-ticks, +.timeline-marker-waterfall { + /* Background created on a <canvas> in js. */ + /* @see browser/devtools/timeline/widgets/waterfall.js */ + background-image: -moz-element(#waterfall-background); + background-repeat: repeat-y; + background-position: -1px center; +} + +.timeline-marker-waterfall { + overflow: hidden; +} + +.timeline-marker-container[is-spacer] { + pointer-events: none; +} + +.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) { + background-color: rgba(255,255,255,0.03); +} + +.theme-light .timeline-marker-container:not([is-spacer]):nth-child(2n) { + background-color: rgba(128,128,128,0.03); +} + +.theme-dark .timeline-marker-container:hover { + background-color: rgba(255,255,255,0.1) !important; +} + +.theme-light .timeline-marker-container:hover { + background-color: rgba(128,128,128,0.1) !important; +} + +.timeline-header-sidebar, +.timeline-marker-sidebar { + -moz-border-end: 1px solid; +} + +.theme-dark .timeline-header-sidebar, +.theme-dark .timeline-marker-sidebar { + -moz-border-end-color: #000; +} + +.theme-light .timeline-header-sidebar, +.theme-light .timeline-marker-sidebar { + -moz-border-end-color: #aaa; +} + +.timeline-header-sidebar { + padding: 5px; +} + +.timeline-marker-sidebar { + padding: 2px; +} + +.timeline-marker-container:hover > .timeline-marker-sidebar { + background-color: transparent; +} + +.timeline-header-tick { + width: 100px; + font-size: 9px; + transform-origin: left center; +} + +.theme-dark .timeline-header-tick { + color: #a9bacb; +} + +.theme-light .timeline-header-tick { + color: #292e33; +} + +.timeline-header-tick:not(:first-child) { + -moz-margin-start: -100px !important; /* Don't affect layout. */ +} + +.timeline-marker-bullet { + width: 8px; + height: 8px; + -moz-margin-start: 8px; + -moz-margin-end: 6px; + border: 1px solid; + border-radius: 1px; +} + +.timeline-marker-bar { + margin-top: 4px; + margin-bottom: 4px; + border: 1px solid; + border-radius: 1px; + transform-origin: left center; +}
new file mode 100644 --- /dev/null +++ b/browser/themes/windows/devtools/timeline.css @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +%include ../../shared/devtools/timeline.inc.css
--- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -279,16 +279,17 @@ browser.jar: skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png) skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png) skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png) skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css) * skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css) * skin/classic/browser/devtools/debugger.css (devtools/debugger.css) * skin/classic/browser/devtools/netmonitor.css (devtools/netmonitor.css) * skin/classic/browser/devtools/profiler.css (devtools/profiler.css) +* skin/classic/browser/devtools/timeline.css (devtools/timeline.css) * skin/classic/browser/devtools/scratchpad.css (devtools/scratchpad.css) * skin/classic/browser/devtools/shadereditor.css (devtools/shadereditor.css) skin/classic/browser/devtools/storage.css (../shared/devtools/storage.css) * skin/classic/browser/devtools/splitview.css (../shared/devtools/splitview.css) skin/classic/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css) * skin/classic/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css) skin/classic/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png) skin/classic/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png) @@ -699,16 +700,17 @@ browser.jar: skin/classic/aero/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png) skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png) skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png) * skin/classic/aero/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css) * skin/classic/aero/browser/devtools/debugger.css (devtools/debugger.css) skin/classic/aero/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css) * skin/classic/aero/browser/devtools/netmonitor.css (devtools/netmonitor.css) * skin/classic/aero/browser/devtools/profiler.css (devtools/profiler.css) +* skin/classic/aero/browser/devtools/timeline.css (devtools/timeline.css) * skin/classic/aero/browser/devtools/scratchpad.css (devtools/scratchpad.css) * skin/classic/aero/browser/devtools/shadereditor.css (devtools/shadereditor.css) * skin/classic/aero/browser/devtools/splitview.css (../shared/devtools/splitview.css) skin/classic/aero/browser/devtools/styleeditor.css (../shared/devtools/styleeditor.css) skin/classic/aero/browser/devtools/storage.css (../shared/devtools/storage.css) * skin/classic/aero/browser/devtools/webaudioeditor.css (devtools/webaudioeditor.css) skin/classic/aero/browser/devtools/magnifying-glass.png (../shared/devtools/images/magnifying-glass.png) skin/classic/aero/browser/devtools/magnifying-glass@2x.png (../shared/devtools/images/magnifying-glass@2x.png)
--- a/dom/camera/GonkCameraControl.cpp +++ b/dom/camera/GonkCameraControl.cpp @@ -785,62 +785,44 @@ nsGonkCameraControl::SetPictureSizeImpl( return NS_ERROR_INVALID_ARG; } if (aSize.width == mLastPictureSize.width && aSize.height == mLastPictureSize.height) { DOM_CAMERA_LOGI("Requested picture size %ux%u unchanged\n", aSize.width, aSize.height); return NS_OK; } - /** - * Choose the supported picture size that is closest in area to the - * specified size. Some drivers will fail to take a picture if the - * thumbnail size is not the same aspect ratio, so we update that - * as well to a size closest to the last user-requested one. - */ - int smallestDelta = INT_MAX; - uint32_t smallestDeltaIndex = UINT32_MAX; - int targetArea = aSize.width * aSize.height; - nsAutoTArray<Size, 8> supportedSizes; Get(CAMERA_PARAM_SUPPORTED_PICTURESIZES, supportedSizes); - for (uint32_t i = 0; i < supportedSizes.Length(); ++i) { - int area = supportedSizes[i].width * supportedSizes[i].height; - int delta = abs(area - targetArea); - - if (area != 0 && delta < smallestDelta) { - smallestDelta = delta; - smallestDeltaIndex = i; - } - } - - if (smallestDeltaIndex == UINT32_MAX) { + Size best; + nsresult rv = GetSupportedSize(aSize, supportedSizes, best); + if (NS_FAILED(rv)) { DOM_CAMERA_LOGW("Unable to find a picture size close to %ux%u\n", aSize.width, aSize.height); return NS_ERROR_INVALID_ARG; } - Size size = supportedSizes[smallestDeltaIndex]; DOM_CAMERA_LOGI("camera-param set picture-size = %ux%u (requested %ux%u)\n", - size.width, size.height, aSize.width, aSize.height); - if (size.width > INT32_MAX || size.height > INT32_MAX) { + best.width, best.height, aSize.width, aSize.height); + if (best.width > INT32_MAX || best.height > INT32_MAX) { DOM_CAMERA_LOGE("Supported picture size is too big, no change\n"); return NS_ERROR_FAILURE; } - nsresult rv = mParams.Set(CAMERA_PARAM_PICTURE_SIZE, size); + rv = mParams.Set(CAMERA_PARAM_PICTURE_SIZE, best); if (NS_FAILED(rv)) { return rv; } - mLastPictureSize = size; + mLastPictureSize = best; - // Finally, update the thumbnail size in case the picture - // aspect ratio changed. + // Finally, update the thumbnail size in case the picture aspect ratio changed. + // Some drivers will fail to take a picture if the thumbnail size is not the + // same aspect ratio as the picture size. return UpdateThumbnailSize(); } int32_t nsGonkCameraControl::RationalizeRotation(int32_t aRotation) { int32_t r = aRotation; @@ -1278,17 +1260,17 @@ nsGonkCameraControl::SetPreviewSize(cons nsTArray<Size> previewSizes; nsresult rv = Get(CAMERA_PARAM_SUPPORTED_PREVIEWSIZES, previewSizes); if (NS_FAILED(rv)) { DOM_CAMERA_LOGE("Camera failed to return any preview sizes (0x%x)\n", rv); return rv; } Size best; - rv = GetSupportedSize(aSize, previewSizes, best); + rv = GetSupportedSize(aSize, previewSizes, best); if (NS_FAILED(rv)) { DOM_CAMERA_LOGE("Failed to find a supported preview size, requested size %dx%d", aSize.width, aSize.height); return rv; } // Some camera drivers will ignore our preview size if it's larger // than the currently set video recording size, so we need to set @@ -1331,19 +1313,29 @@ nsGonkCameraControl::GetSupportedSize(co nsresult rv = NS_ERROR_INVALID_ARG; best = aSize; uint32_t minSizeDelta = UINT32_MAX; uint32_t delta; if (!aSize.width && !aSize.height) { // no size specified, take the first supported size best = supportedSizes[0]; - rv = NS_OK; + return NS_OK; } else if (aSize.width && aSize.height) { - // both height and width specified, find the supported size closest to requested size + // both height and width specified, find the supported size closest to + // the requested size, looking for an exact match first + for (nsTArray<Size>::index_type i = 0; i < supportedSizes.Length(); i++) { + Size size = supportedSizes[i]; + if (size.width == aSize.width && size.height == aSize.height) { + best = size; + return NS_OK; + } + } + + // no exact matches--look for a match closest in area uint32_t targetArea = aSize.width * aSize.height; for (nsTArray<Size>::index_type i = 0; i < supportedSizes.Length(); i++) { Size size = supportedSizes[i]; uint32_t delta = abs((long int)(size.width * size.height - targetArea)); if (delta < minSizeDelta) { minSizeDelta = delta; best = size; rv = NS_OK;
--- a/dom/camera/test/test_camera_fake_parameters.html +++ b/dom/camera/test/test_camera_fake_parameters.html @@ -331,20 +331,85 @@ var tests = [ "exposureCompensation(-2^32) = " + cam.exposureCompensation); cam.exposureCompensation = Math.pow(2, 32); ok(cam.exposureCompensation == cap.maxExposureCompensation, "exposureCompensation(2^32) = " + cam.exposureCompensation); next(); }, }, + { + key: "bug-1054803", + prep: function setupFakePictureSizes(test) { + // The important part of this test is that 3264 * 1836 = 5,992,704 = 2448 * 2448, + // so we need to make sure that the size-matching algorithm picks the right size. + test.setFakeParameters("picture-size-values=3264x1836,2448x2448,1836x3264", function () { + run(); + }); + }, + test: function testFakeFocusAreas(cam, cap) { + // validate the capability attribute + ok(cap.pictureSizes.length == 3, "pictureSizes.length = " + cap.pictureSizes.length); + var found = 0; + [ { height: 3264, width: 1836 }, + { height: 1836, width: 3264 }, + { height: 2448, width: 2448 } ].forEach(function(size) { + found = 0; + cap.pictureSizes.forEach(function(capSize) { + if (capSize.height == size.height && capSize.width == size.width) { + ++found; + } + }); + ok(found == 1, "found size " + size.toSource() + " in pictureSizes"); + }); + + // test setters and getters + var sync = new Promise(function(resolve, reject) { + // Use setConfiguration() (which will fail on the profile) + // to signify that the CameraControl thread has run and our + // settings are applied. Yes--this is an ugly hack. + cam.setConfiguration({ mode: 'video', + recorderProfile: 'weird-unsupported-profile' + }, resolve, resolve); + }); + var sizeGenerator = function() { + var sizes = [ { height: 3264, width: 1836 }, + { height: 1836, width: 3264 }, + { height: 2448, width: 2448 } ]; + for (var i = 0; i < sizes.length; ++i) { + yield sizes[i]; + } + }(); + + function nextSize() { + try { + var size = sizeGenerator.next(); + cam.setPictureSize(size); + sync.then(function() { + var got = cam.getPictureSize(); + ok(got.width == size.width && got.height == size.height, + "Set size " + size.toSource() + ", got size " + got.toSource()); + nextSize(); + }, onError); + } catch(e) { + if (e instanceof StopIteration) { + next(); + } else { + throw e; + } + } + } + + nextSize(); + }, + }, ]; var testGenerator = function() { - for (var i = 0; i < tests.length; ++i ) { + for (var i = 0; i < tests.length; ++i) { yield tests[i]; } }(); window.addEventListener('beforeunload', function() { document.getElementById('viewfinder').mozSrcObject = null; if (cameraObj) { cameraObj.release();
--- a/dom/nfc/MozNDEFRecord.cpp +++ b/dom/nfc/MozNDEFRecord.cpp @@ -59,28 +59,68 @@ MozNDEFRecord::DropData() mId = nullptr; } if (mPayload) { mPayload = nullptr; } mozilla::DropJSObjects(this); } +/** + * Validate TNF. + * See section 3.3 THE NDEF Specification Test Requirements, + * NDEF specification 1.0 + */ +/* static */ +bool +MozNDEFRecord::ValidateTNF(const MozNDEFRecordOptions& aOptions, + ErrorResult& aRv) +{ + // * The TNF field MUST have a value between 0x00 and 0x06. + // * The TNF value MUST NOT be 0x07. + // These two requirements are already handled by WebIDL bindings. + + // If the TNF value is 0x00 (Empty), the TYPE, ID, and PAYLOAD fields MUST be + // omitted from the record. + if ((aOptions.mTnf == TNF::Empty) && + (aOptions.mType.WasPassed() || aOptions.mId.WasPassed() || + aOptions.mPayload.WasPassed())) { + NS_WARNING("tnf is empty but type/id/payload is not null."); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + // If the TNF value is 0x05 (Unknown) or 0x06(Unchanged), the TYPE field MUST + // be omitted from the NDEF record. + if ((aOptions.mTnf == TNF::Unknown || aOptions.mTnf == TNF::Unchanged) && + aOptions.mType.WasPassed()) { + NS_WARNING("tnf is unknown/unchanged but type is not null."); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + return true; +} + /* static */ already_AddRefed<MozNDEFRecord> MozNDEFRecord::Constructor(const GlobalObject& aGlobal, const MozNDEFRecordOptions& aOptions, ErrorResult& aRv) { nsCOMPtr<nsPIDOMWindow> win = do_QueryInterface(aGlobal.GetAsSupports()); if (!win) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } + if (!ValidateTNF(aOptions, aRv)) { + return nullptr; + } + nsRefPtr<MozNDEFRecord> ndefrecord = new MozNDEFRecord(aGlobal.Context(), win, aOptions); if (!ndefrecord) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } return ndefrecord.forget(); }
--- a/dom/nfc/MozNDEFRecord.h +++ b/dom/nfc/MozNDEFRecord.h @@ -10,16 +10,17 @@ #define mozilla_dom_MozNDEFRecord_h__ #include "mozilla/Attributes.h" #include "mozilla/ErrorResult.h" #include "nsCycleCollectionParticipant.h" #include "nsWrapperCache.h" #include "jsapi.h" +#include "mozilla/dom/MozNDEFRecordBinding.h" #include "mozilla/dom/TypedArray.h" #include "jsfriendapi.h" #include "js/GCAPI.h" #include "nsPIDOMWindow.h" struct JSContext; namespace mozilla { @@ -48,17 +49,17 @@ public: virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE; static already_AddRefed<MozNDEFRecord> Constructor(const GlobalObject& aGlobal, const MozNDEFRecordOptions& aOptions, ErrorResult& aRv); - uint8_t Tnf() const + TNF Tnf() const { return mTnf; } void GetType(JSContext* cx, JS::MutableHandle<JSObject*> retval) const { if (mType) { JS::ExposeObjectToActiveJS(mType); @@ -83,17 +84,20 @@ public: } private: MozNDEFRecord() MOZ_DELETE; nsRefPtr<nsPIDOMWindow> mWindow; void HoldData(); void DropData(); - uint8_t mTnf; + static bool + ValidateTNF(const MozNDEFRecordOptions& aOptions, ErrorResult& aRv); + + TNF mTnf; JS::Heap<JSObject*> mType; JS::Heap<JSObject*> mId; JS::Heap<JSObject*> mPayload; }; } // namespace dom } // namespace mozilla
--- a/dom/nfc/gonk/NfcMessageHandler.cpp +++ b/dom/nfc/gonk/NfcMessageHandler.cpp @@ -1,23 +1,25 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "NfcMessageHandler.h" #include <binder/Parcel.h> +#include "mozilla/dom/MozNDEFRecordBinding.h" #include "nsDebug.h" #include "NfcGonkMessage.h" #include "NfcOptions.h" #include <android/log.h> #define CHROMIUM_LOG(args...) __android_log_print(ANDROID_LOG_INFO, "NfcMessageHandler", args) using namespace android; using namespace mozilla; +using namespace mozilla::dom; static const char* kConfigRequest = "config"; static const char* kGetDetailsNDEF = "getDetailsNDEF"; static const char* kReadNDEFRequest = "readNDEF"; static const char* kWriteNDEFRequest = "writeNDEF"; static const char* kMakeReadOnlyNDEFRequest = "makeReadOnlyNDEF"; static const char* kConnectRequest = "connect"; static const char* kCloseRequest = "close"; @@ -325,17 +327,17 @@ bool NfcMessageHandler::ReadNDEFMessage(const Parcel& aParcel, EventOptions& aOptions) { int32_t recordCount = aParcel.readInt32(); aOptions.mRecords.SetCapacity(recordCount); for (int i = 0; i < recordCount; i++) { int32_t tnf = aParcel.readInt32(); NDEFRecordStruct record; - record.mTnf = tnf; + record.mTnf = static_cast<TNF>(tnf); int32_t typeLength = aParcel.readInt32(); record.mType.AppendElements( static_cast<const uint8_t*>(aParcel.readInplace(typeLength)), typeLength); int32_t idLength = aParcel.readInt32(); record.mId.AppendElements( static_cast<const uint8_t*>(aParcel.readInplace(idLength)), idLength); @@ -352,17 +354,17 @@ NfcMessageHandler::ReadNDEFMessage(const bool NfcMessageHandler::WriteNDEFMessage(Parcel& aParcel, const CommandOptions& aOptions) { int recordCount = aOptions.mRecords.Length(); aParcel.writeInt32(recordCount); for (int i = 0; i < recordCount; i++) { const NDEFRecordStruct& record = aOptions.mRecords[i]; - aParcel.writeInt32(record.mTnf); + aParcel.writeInt32(static_cast<int32_t>(record.mTnf)); void* data; aParcel.writeInt32(record.mType.Length()); data = aParcel.writeInplace(record.mType.Length()); memcpy(data, record.mType.Elements(), record.mType.Length()); aParcel.writeInt32(record.mId.Length());
--- a/dom/nfc/gonk/NfcOptions.h +++ b/dom/nfc/gonk/NfcOptions.h @@ -1,22 +1,23 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef NfcOptions_h #define NfcOptions_h #include "mozilla/dom/NfcOptionsBinding.h" +#include "mozilla/dom/MozNDEFRecordBinding.h" namespace mozilla { struct NDEFRecordStruct { - uint8_t mTnf; + dom::TNF mTnf; nsTArray<uint8_t> mType; nsTArray<uint8_t> mId; nsTArray<uint8_t> mPayload; }; struct CommandOptions { CommandOptions(const mozilla::dom::NfcCommandOptions& aOther) {
--- a/dom/nfc/gonk/NfcService.cpp +++ b/dom/nfc/gonk/NfcService.cpp @@ -20,17 +20,17 @@ #define NS_NFCSERVICE_CONTRACTID "@mozilla.org/nfc/service;1" using namespace android; using namespace mozilla::dom; using namespace mozilla::ipc; static const nsLiteralString SEOriginString[] = { NS_LITERAL_STRING("SIM"), - NS_LITERAL_STRING("ESE"), + NS_LITERAL_STRING("eSE"), NS_LITERAL_STRING("ASSD") }; namespace mozilla { static NfcService* gNfcService; NS_IMPL_ISUPPORTS(NfcService, nsINfcService) @@ -128,16 +128,17 @@ public: return NS_ERROR_FAILURE; } for (int i = 0; i < length; i++) { NDEFRecordStruct& recordStruct = mEvent.mRecords[i]; MozNDEFRecordOptions& record = *event.mRecords.Value().AppendElement(); record.mTnf = recordStruct.mTnf; + MOZ_ASSERT(record.mTnf < TNF::EndGuard_); if (recordStruct.mType.Length() > 0) { record.mType.Construct(); record.mType.Value().Init(Uint8Array::Create(cx, recordStruct.mType.Length(), recordStruct.mType.Elements())); } if (recordStruct.mId.Length() > 0) { record.mId.Construct();
--- a/dom/nfc/nsNfc.js +++ b/dom/nfc/nsNfc.js @@ -17,18 +17,16 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "appsService", "@mozilla.org/AppsService;1", "nsIAppsService"); -const NFC_PEER_EVENT_READY = 0x01; -const NFC_PEER_EVENT_LOST = 0x02; /** * NFCTag */ function MozNFCTag() { debug("In MozNFCTag Constructor"); this._nfcContentHelper = Cc["@mozilla.org/nfc/content-helper;1"] .getService(Ci.nsINfcContentHelper); @@ -221,29 +219,27 @@ mozNfc.prototype = { get onpeerlost() { return this.__DOM_IMPL__.getEventHandler("onpeerlost"); }, set onpeerlost(handler) { this.__DOM_IMPL__.setEventHandler("onpeerlost", handler); }, - eventListenerWasAdded: function(evt) { - let eventType = this.getEventType(evt); - if (eventType != NFC_PEER_EVENT_READY) { + eventListenerWasAdded: function(eventType) { + if (eventType !== "peerready") { return; } let appId = this._window.document.nodePrincipal.appId; this._nfcContentHelper.registerTargetForPeerReady(this._window, appId); }, - eventListenerWasRemoved: function(evt) { - let eventType = this.getEventType(evt); - if (eventType != NFC_PEER_EVENT_READY) { + eventListenerWasRemoved: function(eventType) { + if (eventType !== "peerready") { return; } let appId = this._window.document.nodePrincipal.appId; this._nfcContentHelper.unregisterTargetForPeerReady(this._window, appId); }, notifyPeerReady: function notifyPeerReady(sessionToken) { @@ -280,31 +276,16 @@ mozNfc.prototype = { this.session = null; debug("fire onpeerlost"); let event = new this._window.Event("peerlost"); this.__DOM_IMPL__.dispatchEvent(event); }, - getEventType: function getEventType(evt) { - let eventType = -1; - switch (evt) { - case 'peerready': - eventType = NFC_PEER_EVENT_READY; - break; - case 'peerlost': - eventType = NFC_PEER_EVENT_LOST; - break; - default: - break; - } - return eventType; - }, - hasDeadWrapper: function hasDeadWrapper() { return Cu.isDeadWrapper(this._window) || Cu.isDeadWrapper(this.__DOM_IMPL__); }, classID: Components.ID("{6ff2b290-2573-11e3-8224-0800200c9a66}"), contractID: "@mozilla.org/navigatorNfc;1", QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer,
--- a/dom/nfc/tests/marionette/head.js +++ b/dom/nfc/tests/marionette/head.js @@ -121,18 +121,19 @@ let NCI = (function() { LIMIT_NOTIFICATION: 1, MORE_NOTIFICATIONS: 2 }; }()); let TAG = (function() { function setData(re, flag, tnf, type, payload) { let deferred = Promise.defer(); + let tnfNum = NDEF.getTNFNum(tnf); let cmd = "nfc tag set " + re + - " [" + flag + "," + tnf + "," + type + ",," + payload + "]"; + " [" + flag + "," + tnfNum + "," + type + ",," + payload + "]"; emulator.run(cmd, function(result) { is(result.pop(), "OK", "set NDEF data of tag" + re); deferred.resolve(); }); return deferred.promise; }; @@ -151,18 +152,19 @@ let TAG = (function() { setData: setData, clearData: clearData }; }()); let SNEP = (function() { function put(dsap, ssap, flags, tnf, type, id, payload) { let deferred = Promise.defer(); + let tnfNum = NDEF.getTNFNum(tnf); let cmd = "nfc snep put " + dsap + " " + ssap + " [" + flags + "," + - tnf + "," + + tnfNum + "," + type + "," + id + "," + payload + "]"; emulator.run(cmd, function(result) { is(result.pop(), "OK", "send SNEP PUT"); deferred.resolve(); }); @@ -240,17 +242,28 @@ function runTests() { // succeed immediately on systems without NFC log('Skipping test on system without NFC'); ok(true, 'Skipping test on system without NFC'); finish(); } } const NDEF = { - TNF_WELL_KNOWN: 1, + TNF_WELL_KNOWN: "well-known", + + tnfValues: ["empty", "well-known", "media-type", "absolute-uri", "external", + "unknown", "unchanged", "reserved"], + + getTNFNum: function (tnfString) { + return this.tnfValues.indexOf(tnfString); + }, + + getTNFString: function(tnfNum) { + return this.tnfValues[tnfNum]; + }, // compares two NDEF messages compare: function(ndef1, ndef2) { isnot(ndef1, null, "LHS message is not null"); isnot(ndef2, null, "RHS message is not null"); is(ndef1.length, ndef2.length, "NDEF messages have the same number of records"); ndef1.forEach(function(record1, index) { @@ -285,17 +298,17 @@ const NDEF = { ok(false, "Parser error: " + e.message); return null; } // and build NDEF array let ndef = arr.map(function(value) { let type = NfcUtils.fromUTF8(this.atob(value.type)); let id = NfcUtils.fromUTF8(this.atob(value.id)); let payload = NfcUtils.fromUTF8(this.atob(value.payload)); - return new MozNDEFRecord({tnf: value.tnf, type: type, id: id, payload: payload}); + return new MozNDEFRecord({tnf: NDEF.getTNFString(value.tnf), type: type, id: id, payload: payload}); }, window); return ndef; } }; var NfcUtils = { fromUTF8: function(str) { let buf = new Uint8Array(str.length);
--- a/dom/nfc/tests/marionette/test_ndef.js +++ b/dom/nfc/tests/marionette/test_ndef.js @@ -3,26 +3,54 @@ MARIONETTE_TIMEOUT = 30000; MARIONETTE_HEAD_JS = 'head.js'; function testConstructNDEF() { try { // omit type, id and payload. let r = new MozNDEFRecord(); - is(r.tnf, 0, "r.tnf should be 0"); + is(r.tnf, "empty", "r.tnf should be 'empty'"); is(r.type, null, "r.type should be null"); is(r.id, null, "r.id should be null"); is(r.payload, null, "r.payload should be null"); ok(true); } catch (e) { ok(false, 'type, id or payload should be optional. error:' + e); } + try { + new MozNDEFRecord({type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, type should be null for empty tnf"); + } catch (e){ + ok(true); + } + + try { + new MozNDEFRecord({tnf: "unknown", type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, type should be null for unknown tnf"); + } catch (e){ + ok(true); + } + + try { + new MozNDEFRecord({tnf: "unchanged", type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, type should be null for unchanged tnf"); + } catch (e){ + ok(true); + } + + try { + new MozNDEFRecord({tnf: "illegal", type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, invalid tnf"); + } catch (e){ + ok(true); + } + runNextTest(); } let tests = [ testConstructNDEF ]; runTests();
--- a/dom/nfc/tests/marionette/test_nfc_error_messages.js +++ b/dom/nfc/tests/marionette/test_nfc_error_messages.js @@ -5,17 +5,17 @@ /* globals log, is, ok, runTests, toggleNFC, runNextTest, SpecialPowers, nfc, MozNDEFRecord, emulator */ const MARIONETTE_TIMEOUT = 60000; const MARIONETTE_HEAD_JS = 'head.js'; const MANIFEST_URL = 'app://system.gaiamobile.org/manifest.webapp'; -const NDEF_MESSAGE = [new MozNDEFRecord({tnf: 0x01, +const NDEF_MESSAGE = [new MozNDEFRecord({tnf: "well-known", type: new Uint8Array(0x84), payload: new Uint8Array(0x20)})]; let nfcPeers = []; /** * Enables nfc and RE0 then registers onpeerready callback and once * it's fired it creates mozNFCPeer and stores it for later.
--- a/dom/system/gonk/ril_worker.js +++ b/dom/system/gonk/ril_worker.js @@ -3465,24 +3465,16 @@ RilObject.prototype = { cardState: this.cardState}); this.iccInfo = {iccType: null}; this.context.ICCUtilsHelper.handleICCInfoChange(); } return; } - let ICCRecordHelper = this.context.ICCRecordHelper; - // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED. - if (iccStatus.cardState === CARD_STATE_PRESENT && - (this.cardState === GECKO_CARDSTATE_UNINITIALIZED || - this.cardState === GECKO_CARDSTATE_UNDETECTED)) { - ICCRecordHelper.readICCID(); - } - if (RILQUIRKS_SUBSCRIPTION_CONTROL) { // All appIndex is -1 means the subscription is not activated yet. // Note that we don't support "ims" for now, so we don't take it into // account. let neetToActivate = iccStatus.cdmaSubscriptionAppIndex === -1 && iccStatus.gsmUmtsSubscriptionAppIndex === -1; if (neetToActivate && // Note: setUiccSubscription works abnormally when RADIO is OFF, @@ -3533,16 +3525,24 @@ RilObject.prototype = { if (pin1State === CARD_PINSTATE_ENABLED_PERM_BLOCKED) { newCardState = GECKO_CARDSTATE_PERMANENT_BLOCKED; } } else { // Having incorrect app information, set card state to unknown. newCardState = GECKO_CARDSTATE_UNKNOWN; } + let ICCRecordHelper = this.context.ICCRecordHelper; + // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED. + if (iccStatus.cardState === CARD_STATE_PRESENT && + (this.cardState === GECKO_CARDSTATE_UNINITIALIZED || + this.cardState === GECKO_CARDSTATE_UNDETECTED)) { + ICCRecordHelper.readICCID(); + } + if (this.cardState == newCardState) { return; } // This was moved down from CARD_APPSTATE_READY this.requestNetworkInfo(); if (newCardState == GECKO_CARDSTATE_READY) { // For type SIM, we need to check EF_phase first. @@ -5849,17 +5849,21 @@ RilObject.prototype[REQUEST_SIM_IO] = fu return; } // Don't need to read rilRequestError since we can know error status from // sw1 and sw2. let Buf = this.context.Buf; options.sw1 = Buf.readInt32(); options.sw2 = Buf.readInt32(); - if (options.sw1 != ICC_STATUS_NORMAL_ENDING) { + // See 3GPP TS 11.11, clause 9.4.1 for opetation success results. + if (options.sw1 !== ICC_STATUS_NORMAL_ENDING && + options.sw1 !== ICC_STATUS_NORMAL_ENDING_WITH_EXTRA && + options.sw1 !== ICC_STATUS_WITH_SIM_DATA && + options.sw1 !== ICC_STATUS_WITH_RESPONSE_DATA) { ICCIOHelper.processICCIOError(options); return; } ICCIOHelper.processICCIO(options); }; RilObject.prototype[REQUEST_SEND_USSD] = function REQUEST_SEND_USSD(length, options) { if (DEBUG) { this.context.debug("REQUEST_SEND_USSD " + JSON.stringify(options)); @@ -12700,16 +12704,17 @@ ICCIOHelperObject.prototype = { options.p3 = 0x00; break; // For RUIM, CSIM and ISIM, cf bug 955946: keep the old behavior case CARD_APPTYPE_RUIM: case CARD_APPTYPE_CSIM: case CARD_APPTYPE_ISIM: // For SIM, this is what we want case CARD_APPTYPE_SIM: + default: options.p2 = 0x00; options.p3 = GET_RESPONSE_EF_SIZE_BYTES; break; } this.context.RIL.iccIO(options); }, /**
--- a/dom/webidl/MozNDEFRecord.webidl +++ b/dom/webidl/MozNDEFRecord.webidl @@ -1,31 +1,33 @@ /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Copyright © 2013 Deutsche Telekom, Inc. */ +enum TNF { + "empty", + "well-known", + "media-type", + "absolute-uri", + "external", + "unknown", + "unchanged" +}; + [Constructor(optional MozNDEFRecordOptions options)] interface MozNDEFRecord { /** - * Type Name Field (3-bits) - Specifies the NDEF record type in general. - * tnf_empty: 0x00 - * tnf_well_known: 0x01 - * tnf_mime_media: 0x02 - * tnf_absolute_uri: 0x03 - * tnf_external type: 0x04 - * tnf_unknown: 0x05 - * tnf_unchanged: 0x06 - * tnf_reserved: 0x07 + * Type Name Field - Specifies the NDEF record type in general. */ [Constant] - readonly attribute octet tnf; + readonly attribute TNF tnf; /** * type - Describes the content of the payload. This can be a mime type. */ [Constant] readonly attribute Uint8Array? type; /** @@ -38,13 +40,13 @@ interface MozNDEFRecord * payload - Binary data blob. The meaning of this field is application * dependent. */ [Constant] readonly attribute Uint8Array? payload; }; dictionary MozNDEFRecordOptions { - octet tnf = 0; // default to tnf_empty. + TNF tnf = "empty"; Uint8Array type; Uint8Array id; Uint8Array payload; };
--- a/layout/reftests/bugs/reftest.list +++ b/layout/reftests/bugs/reftest.list @@ -959,17 +959,17 @@ fails == 413027-3.html 413027-3-ref.html == 413286-2b.html 413286-2-ref.html == 413286-2c.html 413286-2-ref.html == 413286-3.html 413286-3-ref.html == 413286-4a.html 413286-4-ref.html == 413286-4b.html 413286-4-ref.html == 413286-5.html 413286-5-ref.html == 413286-6.html 413286-6-ref.html skip-if(cocoaWidget) == 413292-1.html 413292-1-ref.html # disabling due to failure loading on some mac tinderboxes. See bug 432954 -== 413361-1.html 413361-1-ref.html +fuzzy-if(Android&&AndroidVersion>=15,11,15) == 413361-1.html 413361-1-ref.html == 413840-background-unchanged.html 413840-background-unchanged-ref.html == 413840-ltr-offsets.html 413840-ltr-offsets-ref.html == 413840-rtl-offsets.html 413840-rtl-offsets-ref.html == 413840-pushed-line-bullet.html 413840-pushed-line-bullet-ref.html == 413840-bullet-first-line.html 413840-bullet-first-line-ref.html == 413982.html 413982-ref.html == 414123.xhtml 414123-ref.xhtml == 414638.html 414638-ref.html
--- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -1515,23 +1515,22 @@ public class BrowserApp extends GeckoApp } } catch (Exception e) { Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); } } @Override public void addTab() { - // Always load about:home when opening a new tab. - Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB); + Tabs.getInstance().addTab(); } @Override public void addPrivateTab() { - Tabs.getInstance().loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE); + Tabs.getInstance().addPrivateTab(); } @Override public void showNormalTabs() { showTabs(TabsPanel.Panel.NORMAL_TABS); } @Override @@ -2552,22 +2551,22 @@ public class BrowserApp extends GeckoApp if (AboutPages.isAboutReader(url)) { String urlFromReader = ReaderModeUtils.getUrlFromAboutReader(url); if (urlFromReader != null) { url = urlFromReader; } } // Disable share menuitem for about:, chrome:, file:, and resource: URIs - final boolean inGuestMode = GeckoProfile.get(this).inGuestMode(); - share.setVisible(!inGuestMode); - share.setEnabled(StringUtils.isShareableUrl(url) && !inGuestMode); - MenuUtils.safeSetEnabled(aMenu, R.id.apps, !inGuestMode); - MenuUtils.safeSetEnabled(aMenu, R.id.addons, !inGuestMode); - MenuUtils.safeSetEnabled(aMenu, R.id.downloads, !inGuestMode); + final boolean shareEnabled = RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_SHARE); + share.setVisible(shareEnabled); + share.setEnabled(StringUtils.isShareableUrl(url) && shareEnabled); + MenuUtils.safeSetEnabled(aMenu, R.id.apps, RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_INSTALL_APPS)); + MenuUtils.safeSetEnabled(aMenu, R.id.addons, RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_INSTALL_EXTENSIONS)); + MenuUtils.safeSetEnabled(aMenu, R.id.downloads, RestrictedProfiles.isAllowed(RestrictedProfiles.Restriction.DISALLOW_DOWNLOADS)); // NOTE: Use MenuUtils.safeSetEnabled because some actions might // be on the BrowserToolbar context menu. if (Versions.feature11Plus) { MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab)); } MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds()); MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
--- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -99,17 +99,16 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; -import android.os.UserManager; import android.os.Vibrator; import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.view.ContextThemeWrapper; @@ -2549,49 +2548,16 @@ public class GeckoAppShell return "PROXY " + proxy.address().toString(); case SOCKS: return "SOCKS " + proxy.address().toString(); } return "DIRECT"; } - @WrapElementForJNI - public static boolean isUserRestricted() { - if (Versions.preJBMR2) { - return false; - } - - UserManager mgr = (UserManager)getContext().getSystemService(Context.USER_SERVICE); - Bundle restrictions = mgr.getUserRestrictions(); - - return !restrictions.isEmpty(); - } - - @WrapElementForJNI - public static String getUserRestrictions() { - if (Versions.preJBMR2) { - return "{}"; - } - - JSONObject json = new JSONObject(); - UserManager mgr = (UserManager)getContext().getSystemService(Context.USER_SERVICE); - Bundle restrictions = mgr.getUserRestrictions(); - - Set<String> keys = restrictions.keySet(); - for (String key : keys) { - try { - json.put(key, restrictions.get(key)); - } catch (JSONException e) { - } - } - - return json.toString(); - } - /* Downloads the uri pointed to by a share intent, and alters the intent to point to the locally stored file. */ public static void downloadImageForIntent(final Intent intent) { final String src = intent.getStringExtra(Intent.EXTRA_TEXT); final File dir = GeckoApp.getTempDirectory(); if (dir == null) { showImageShareFailureToast();
new file mode 100644 --- /dev/null +++ b/mobile/android/base/RestrictedProfiles.java @@ -0,0 +1,132 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.content.Context; +import android.os.Bundle; +import android.os.UserManager; +import android.util.Log; + +import java.lang.StringBuilder; +import java.util.HashSet; +import java.util.Set; + +import org.json.JSONException; +import org.json.JSONObject; + +import org.mozilla.gecko.AppConstants.Versions; +import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; + + +public class RestrictedProfiles { + private static final String LOGTAG = "GeckoRestrictedProfiles"; + + // These constants should be in sync with the ones from toolkit/components/parentalcontrols/nsIParentalControlServices.java + public static enum Restriction { + DISALLOW_DOWNLOADS(1, "no_download_files"), + DISALLOW_INSTALL_EXTENSIONS(2, "no_install_extensions"), + DISALLOW_INSTALL_APPS(3, UserManager.DISALLOW_INSTALL_APPS), + DISALLOW_BROWSE_FILES(4, "no_browse_files"), + DISALLOW_SHARE(5, "no_share"), + DISALLOW_BOOKMARK(6, "no_bookmark"), + DISALLOW_ADD_CONTACTS(7, "no_add_contacts"), + DISALLOW_SET_IMAGE(8, "no_set_image"); + + public final int id; + public final String name; + + private Restriction(final int id, final String name) { + this.id = id; + this.name = name; + } + } + + private static String geckoActionToRestrction(int action) { + for (Restriction rest : Restriction.values()) { + if (rest.id == action) { + return rest.name; + } + } + + throw new IllegalArgumentException("Unknown action " + action); + } + + private static Bundle getRestrctions() { + final UserManager mgr = (UserManager) GeckoAppShell.getContext().getSystemService(Context.USER_SERVICE); + return mgr.getUserRestrictions(); + } + + @WrapElementForJNI + public static boolean isUserRestricted() { + // Guest mode is supported in all Android versions + if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) { + return true; + } + + if (Versions.preJBMR2) { + return false; + } + + return !getRestrctions().isEmpty(); + } + + public static boolean isAllowed(Restriction action) { + return isAllowed(action.id, null); + } + + @WrapElementForJNI + public static boolean isAllowed(int action, String url) { + // ALl actions are blocked in Guest mode + if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) { + return false; + } + + if (Versions.preJBMR2) { + return true; + } + + try { + final String restriction = geckoActionToRestrction(action); + return !getRestrctions().getBoolean(restriction, false); + } catch(IllegalArgumentException ex) { + Log.i(LOGTAG, "Invalid action", ex); + } + + return true; + } + + @WrapElementForJNI + public static String getUserRestrictions() { + // Guest mode is supported in all Android versions + if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) { + StringBuilder builder = new StringBuilder("{ "); + + for (Restriction restriction : Restriction.values()) { + builder.append("\"" + restriction.name + "\": true, "); + } + + builder.append(" }"); + return builder.toString(); + } + + if (Versions.preJBMR2) { + return "{}"; + } + + final JSONObject json = new JSONObject(); + final Bundle restrictions = getRestrctions(); + final Set<String> keys = restrictions.keySet(); + + for (String key : keys) { + try { + json.put(key, restrictions.get(key)); + } catch (JSONException e) { + } + } + + return json.toString(); + } +} \ No newline at end of file
--- a/mobile/android/base/Tabs.java +++ b/mobile/android/base/Tabs.java @@ -836,16 +836,24 @@ public class Tabs implements GeckoEventL if (AboutPages.isBuiltinIconPage(url)) { Log.d(LOGTAG, "Setting about: tab favicon inline."); added.updateFavicon(getAboutPageFavicon(url)); } return added; } + public Tab addTab() { + return loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB); + } + + public Tab addPrivateTab() { + return loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE); + } + /** * These favicons are only used for the URL bar, so * we fetch with that size. * * This method completes on the calling thread. */ private Bitmap getAboutPageFavicon(final String url) { int faviconSize = Math.round(mAppContext.getResources().getDimension(R.dimen.browser_toolbar_favicon_size));
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -364,16 +364,17 @@ gbjar.sources += [ 'prompts/PromptInput.java', 'prompts/PromptListAdapter.java', 'prompts/PromptListItem.java', 'prompts/PromptService.java', 'prompts/TabInput.java', 'ReaderModeUtils.java', 'RemoteTabsExpandableListAdapter.java', 'Restarter.java', + 'RestrictedProfiles.java', 'ScrollAnimator.java', 'ServiceNotificationClient.java', 'SessionParser.java', 'SharedPreferencesHelper.java', 'SiteIdentity.java', 'SmsManager.java', 'sqlite/ByteBufferInputStream.java', 'sqlite/MatrixBlobCursor.java',
--- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -669,17 +669,19 @@ OnSharedPreferenceChangeListener preferences.removePreference(pref); i--; continue; } else if (!AppConstants.MOZ_CRASHREPORTER && PREFS_CRASHREPORTER_ENABLED.equals(key)) { preferences.removePreference(pref); i--; continue; - } else if (AppConstants.RELEASE_BUILD && PREFS_GEO_REPORTING.equals(key)) { + } else if (AppConstants.RELEASE_BUILD && + (PREFS_GEO_REPORTING.equals(key) || + PREFS_GEO_LEARN_MORE.equals(key))) { // We don't build wifi/cell tower collection in release builds, so hide the UI. preferences.removePreference(pref); i--; continue; } else if (PREFS_DEVTOOLS_REMOTE_ENABLED.equals(key)) { final Context thisContext = this; pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override
--- a/mobile/android/base/preferences/LocaleListPreference.java +++ b/mobile/android/base/preferences/LocaleListPreference.java @@ -58,20 +58,17 @@ public class LocaleListPreference extend Canvas c = new Canvas(b); c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint); return b; } private static byte[] getPixels(final Bitmap b) { final int byteCount; if (Versions.feature19Plus) { - // TODO: when Bug 1042829 lands, do the right thing for KitKat devices. - // Which is: - // byteCount = b.getAllocationByteCount(); - byteCount = b.getRowBytes() * b.getHeight(); + byteCount = b.getAllocationByteCount(); } else { // Close enough for government work. // Equivalent to getByteCount, but works on <12. byteCount = b.getRowBytes() * b.getHeight(); } final ByteBuffer buffer = ByteBuffer.allocate(byteCount); try { @@ -267,18 +264,17 @@ public class LocaleListPreference extend final String value = getValue(); if (TextUtils.isEmpty(value)) { return getContext().getString(R.string.locale_system_default); } // We can't trust super.getSummary() across locale changes, // apparently, so let's do the same work. - final Locale loc = new Locale(value); - return loc.getDisplayName(loc); + return new LocaleDescriptor(value).getDisplayName(); } private void buildList() { final Locale currentLocale = Locale.getDefault(); Log.d(LOG_TAG, "Building locales list. Current locale: " + currentLocale); if (currentLocale.equals(this.entriesLocale) && getEntries() != null) {
--- a/mobile/android/base/tests/testRestrictedProfiles.js +++ b/mobile/android/base/tests/testRestrictedProfiles.js @@ -13,33 +13,43 @@ add_task(function test_isUserRestricted( do_check_true("@mozilla.org/parental-controls-service;1" in Cc); let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService); // In an admin profile, like the tests: enabled = false // In a restricted profile: enabled = true do_check_false(pc.parentalControlsEnabled); - //run_next_test(); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.DOWNLOAD)); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_EXTENSION)); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_APP)); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.VISIT_FILE_URLS)); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.SHARE)); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.BOOKMARK)); + do_check_true(pc.isAllowed(Ci.nsIParentalControlsService.INSTALL_EXTENSION)); + + run_next_test(); }); -/* -// NOTE: JNI.jsm has no way to call a string method yet + add_task(function test_getUserRestrictions() { // In an admin profile, like the tests: {} // In a restricted profile: {"no_modify_accounts":true,"no_share_location":true} let restrictions = "{}"; - let jni = null; + var jenv = null; try { - jni = new JNI(); - let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell"); - let method = jni.getStaticMethodID(cls, "getUserRestrictions", "()Ljava/lang/String;"); - restrictions = jni.callStaticStringMethod(cls, method); + jenv = JNI.GetForThread(); + var geckoAppShell = JNI.LoadClass(jenv, "org.mozilla.gecko.RestrictedProfile", { + static_methods: [ + { name: "getUserRestrictions", sig: "()Ljava/lang/String;" }, + ], + }); + restrictions = JNI.ReadString(jenv, geckoAppShell.getUserRestrictions()); } finally { - if (jni != null) { - jni.close(); + if (jenv) { + JNI.UnloadClasses(jenv); } } do_check_eq(restrictions, "{}"); }); -*/ + run_next_test();
--- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -647,16 +647,20 @@ var SelectionHandler = { id: "share_action", icon: "drawable://ic_menu_share", action: function() { SelectionHandler.shareSelection(); UITelemetry.addEvent("action.1", "actionbar", null, "share"); }, selector: { matches: function() { + if (!ParentalControls.isAllowed(ParentalControls.SHARE)) { + return false; + } + return SelectionHandler.isSelectionActive(); } } }, SEARCH: { label: function() { return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -159,16 +159,19 @@ XPCOMUtils.defineLazyModuleGetter(this, notifications.forEach(notification => { Services.obs.addObserver(observer, notification, false); }); }); XPCOMUtils.defineLazyServiceGetter(this, "Haptic", "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback"); +XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls", + "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService"); + XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils", "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); XPCOMUtils.defineLazyServiceGetter(window, "URIFixup", "@mozilla.org/docshell/urifixup;1", "nsIURIFixup"); #ifdef MOZ_WEBRTC XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", @@ -282,17 +285,16 @@ var Strings = {}; const kFormHelperModeDisabled = 0; const kFormHelperModeEnabled = 1; const kFormHelperModeDynamic = 2; // disabled on tablets var BrowserApp = { _tabs: [], _selectedTab: null, _prefObservers: [], - isGuest: false, get isTablet() { let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); delete this.isTablet; return this.isTablet = sysInfo.get("tablet"); }, get isOnLowMemoryPlatform() { @@ -429,18 +431,16 @@ var BrowserApp = { if (window.arguments[0]) url = window.arguments[0]; if (window.arguments[1]) gScreenWidth = window.arguments[1]; if (window.arguments[2]) gScreenHeight = window.arguments[2]; if (window.arguments[3]) pinned = window.arguments[3]; - if (window.arguments[4]) - this.isGuest = window.arguments[4]; } if (pinned) { this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl)); } else { SearchEngines.init(); this.initContextMenu(); } @@ -454,17 +454,17 @@ var BrowserApp = { // Broadcast a UIReady message so add-ons know we are finished with startup let event = document.createEvent("Events"); event.initEvent("UIReady", true, false); window.dispatchEvent(event); if (this._startupStatus) this.onAppUpdated(); - if (this.isGuest) { + if (!ParentalControls.isAllowed(ParentalControls.INSTALL_EXTENSIONS)) { // Disable extension installs Services.prefs.setIntPref("extensions.enabledScopes", 1); Services.prefs.setIntPref("extensions.autoDisableScopes", 1); Services.prefs.setBoolPref("xpinstall.enabled", false); } // notify java that gecko has loaded Messaging.sendRequest({ type: "Gecko:Ready" }); @@ -576,33 +576,33 @@ var BrowserApp = { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let phoneNumber = NativeWindow.contextmenus._stripScheme(url); NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber); }); NativeWindow.contextmenus.add({ label: Strings.browser.GetStringFromName("contextmenu.shareLink"), order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items - selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext), + selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.linkShareableContext), showAsActions: function(aElement) { return { title: aElement.textContent.trim() || aElement.title.trim(), uri: NativeWindow.contextmenus._getLinkURL(aElement), }; }, icon: "drawable://ic_menu_share", callback: function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link"); } }); NativeWindow.contextmenus.add({ label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"), order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, - selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), + selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.emailLinkContext), showAsActions: function(aElement) { let url = NativeWindow.contextmenus._getLinkURL(aElement); let emailAddr = NativeWindow.contextmenus._stripScheme(url); let title = aElement.textContent || aElement.title; return { title: title, uri: emailAddr, }; @@ -611,58 +611,58 @@ var BrowserApp = { callback: function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email"); } }); NativeWindow.contextmenus.add({ label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"), order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, - selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), + selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.phoneNumberLinkContext), showAsActions: function(aElement) { let url = NativeWindow.contextmenus._getLinkURL(aElement); let phoneNumber = NativeWindow.contextmenus._stripScheme(url); let title = aElement.textContent || aElement.title; return { title: title, uri: phoneNumber, }; }, icon: "drawable://ic_menu_share", callback: function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone"); } }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), - NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), + NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext), function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email"); let url = NativeWindow.contextmenus._getLinkURL(aTarget); Messaging.sendRequest({ type: "Contact:Add", email: url }); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), - NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), + NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext), function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone"); let url = NativeWindow.contextmenus._getLinkURL(aTarget); Messaging.sendRequest({ type: "Contact:Add", phone: url }); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"), - NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext), + NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext), function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark"); let url = NativeWindow.contextmenus._getLinkURL(aTarget); let title = aTarget.textContent || aTarget.title || url; Messaging.sendRequest({ type: "Bookmark:Insert", url: url, @@ -689,17 +689,17 @@ var BrowserApp = { function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media"); aTarget.setAttribute("controls", true); }); NativeWindow.contextmenus.add({ label: Strings.browser.GetStringFromName("contextmenu.shareMedia"), order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, - selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")), + selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.SelectorContext("video")), showAsActions: function(aElement) { let url = (aElement.currentSrc || aElement.src); let title = aElement.textContent || aElement.title; return { title: title, uri: url, type: "video/*", }; @@ -737,17 +737,17 @@ var BrowserApp = { UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image"); let url = aTarget.src; NativeWindow.contextmenus._copyStringToDefaultClipboard(url); }); NativeWindow.contextmenus.add({ label: Strings.browser.GetStringFromName("contextmenu.shareImage"), - selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), + selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.imageSaveableContext), order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items showAsActions: function(aTarget) { let doc = aTarget.ownerDocument; let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) .getImgCacheForDocument(doc); let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet); let src = aTarget.src; return { @@ -769,17 +769,17 @@ var BrowserApp = { UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image"); ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle", false, true, aTarget.ownerDocument.documentURIObject, aTarget.ownerDocument); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"), - NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), + NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext), function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image"); let src = aTarget.src; Messaging.sendRequest({ type: "Image:SetAs", url: src }); @@ -2675,21 +2675,23 @@ var NativeWindow = { try { let url = this._getLinkURL(aElement); return Services.io.newURI(url, null, null); } catch (e) {} } return null; }, - _disableInGuest: function _disableInGuest(selector) { + _disableRestricted: function _disableRestricted(restriction, selector) { return { - matches: function _disableInGuestMatches(aElement, aX, aY) { - if (BrowserApp.isGuest) + matches: function _disableRestrictedMatches(aElement, aX, aY) { + if (!ParentalControls.isAllowed(ParentalControls[restriction])) { return false; + } + return selector.matches(aElement, aX, aY); } }; }, _getLinkURL: function ch_getLinkURL(aLink) { let href = aLink.href; if (href) @@ -4197,18 +4199,18 @@ Tab.prototype = { this._hostChanged = true; let fixedURI = aLocationURI; try { fixedURI = URIFixup.createExposableURI(aLocationURI); } catch (ex) { } - // In guest sessions, we refuse to let you open any file urls. - if (BrowserApp.isGuest) { + // In restricted profiles, we refuse to let you open any file urls. + if (!ParentalControls.isAllowed(ParentalControls.VISIT_FILE_URLS)) { let bannedSchemes = ["file", "chrome", "resource", "jar", "wyciwyg"]; if (bannedSchemes.indexOf(fixedURI.scheme) > -1) { aRequest.cancel(Cr.NS_BINDING_ABORTED); aRequest = this.browser.docShell.displayLoadError(Cr.NS_ERROR_UNKNOWN_PROTOCOL, fixedURI, null); if (aRequest) { fixedURI = aRequest.URI;
--- a/mobile/android/chrome/content/downloads.js +++ b/mobile/android/chrome/content/downloads.js @@ -251,21 +251,22 @@ AlertDownloadProgressListener.prototype Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [PAUSE_BUTTON, CANCEL_BUTTON])); }, onDownloadStateChange: function(aState, aDownload) { let state = aDownload.state; switch (state) { case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: { - if (BrowserApp.isGuest) { + if (!ParentalControls.isAllowed(ParentalControls.DOWNLOADS)) { aDownload.cancel(); NativeWindow.toast.show(Strings.browser.GetStringFromName("downloads.disabledInGuest"), "long"); return; } + NativeWindow.toast.show(Strings.browser.GetStringFromName("alertDownloadsToast"), "long"); Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload, Strings.browser.GetStringFromName("alertDownloadsStart2"), aDownload.displayName)); break; } case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: { Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [RESUME_BUTTON, CANCEL_BUTTON]));
--- a/mobile/android/components/BrowserCLH.js +++ b/mobile/android/components/BrowserCLH.js @@ -13,34 +13,31 @@ Cu.import("resource://gre/modules/Servic function dump(a) { Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a); } function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) { let argsArray = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray); let urlString = null; let pinnedBool = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); - let guestBool = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); let widthInt = Cc["@mozilla.org/supports-PRInt32;1"].createInstance(Ci.nsISupportsPRInt32); let heightInt = Cc["@mozilla.org/supports-PRInt32;1"].createInstance(Ci.nsISupportsPRInt32); if ("url" in aArgs) { urlString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); urlString.data = aArgs.url; } widthInt.data = "width" in aArgs ? aArgs.width : 1; heightInt.data = "height" in aArgs ? aArgs.height : 1; pinnedBool.data = "pinned" in aArgs ? aArgs.pinned : false; - guestBool.data = "guest" in aArgs ? aArgs["guest"] : false; argsArray.AppendElement(urlString, false); argsArray.AppendElement(widthInt, false); argsArray.AppendElement(heightInt, false); argsArray.AppendElement(pinnedBool, false); - argsArray.AppendElement(guestBool, false); return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argsArray); } function resolveURIInternal(aCmdLine, aArgument) { let uri = aCmdLine.resolveURI(aArgument); if (uri) return uri; @@ -56,30 +53,26 @@ function resolveURIInternal(aCmdLine, aA } function BrowserCLH() {} BrowserCLH.prototype = { handle: function fs_handle(aCmdLine) { let openURL = "about:home"; let pinned = false; - let guest = false; let width = 1; let height = 1; try { openURL = aCmdLine.handleFlagWithParam("url", false); } catch (e) { /* Optional */ } try { pinned = aCmdLine.handleFlag("webapp", false); } catch (e) { /* Optional */ } - try { - guest = aCmdLine.handleFlag("guest", false); - } catch (e) { /* Optional */ } try { width = aCmdLine.handleFlagWithParam("width", false); } catch (e) { /* Optional */ } try { height = aCmdLine.handleFlagWithParam("height", false); } catch (e) { /* Optional */ } @@ -97,17 +90,16 @@ BrowserCLH.prototype = { browserWin.browserDOMWindow.openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); } } else { let args = { url: openURL, pinned: pinned, width: width, height: height, - guest: guest }; // Make sure webapps do not have: locationbar, personalbar, menubar, statusbar, and toolbar let flags = "chrome,dialog=no"; if (!pinned) flags += ",all"; browserWin = openWindow(null, "chrome://browser/content/browser.xul", "_blank", flags, args);
--- a/mobile/android/locales/en-US/chrome/webapp.properties +++ b/mobile/android/locales/en-US/chrome/webapp.properties @@ -55,9 +55,9 @@ installUpdateMessage2=Touch to install u retrievalFailedTitle=#1 update failed;#1 updates failed # LOCALIZATION NOTE (retrievalFailedMessage): Semi-colon list of plural forms. # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals # %1$S is a comma-separated list of apps for which retrieval failed. # example: Failed to retrieve updates for Foo, Bar, Baz retrievalFailedMessage=Failed to retrieve update for %1$S;Failed to retrieve updates for %1$S -webappsDisabledInGuest=Installing apps is disabled in guest sessions +webappsDisabled=Installing apps is disabled
--- a/mobile/android/modules/WebappManager.jsm +++ b/mobile/android/modules/WebappManager.jsm @@ -24,16 +24,19 @@ Cu.import("resource://gre/modules/Task.j XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyGetter(this, "Strings", function() { return Services.strings.createBundle("chrome://browser/locale/webapp.properties"); }); +XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls", + "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService"); + /** * Get the formatted plural form of a string. Escapes semicolons in arguments * to provide to the formatter before formatting the string, then unescapes them * after getting its plural form, to avoid tripping up the plural form getter * with a semicolon in one of the formatter's arguments, since the plural forms * of localized strings are delimited by semicolons. * * Ideally, we'd get the plural form first and then format the string, @@ -84,18 +87,18 @@ this.WebappManager = { DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager); return; } this._installApk(aMessage, aMessageManager); }, _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() { - if (this.inGuestSession()) { - aMessage.error = Strings.GetStringFromName("webappsDisabledInGuest"), + if (!ParentalControls.isAllowed(ParentalControls.INSTALL_APPS)) { + aMessage.error = Strings.GetStringFromName("webappsDisabled"), aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage); return; } let filePath; let appName = aMessage.app.manifest ? aMessage.app.manifest.name @@ -272,20 +275,16 @@ this.WebappManager = { // to ensure the user can always remove an app from the registry (and thus // about:apps) even if it's out of sync with installed APKs. debug("APK not installed; proceeding directly to removal from registry"); DOMApplicationRegistry.doUninstall(aData, aMessageManager); } }), - inGuestSession: function() { - return Services.wm.getMostRecentWindow("navigator:browser").BrowserApp.isGuest; - }, - autoInstall: function(aData) { debug("autoInstall " + aData.manifestURL); // If the app is already installed, update the existing installation. // We should be able to use DOMApplicationRegistry.getAppByManifestURL, // but it returns a mozIApplication, while _autoUpdate needs the original // object from DOMApplicationRegistry.webapps in order to modify it. for (let [ , app] in Iterator(DOMApplicationRegistry.webapps)) {
--- a/mobile/android/search/java/org/mozilla/search/Constants.java +++ b/mobile/android/search/java/org/mozilla/search/Constants.java @@ -11,16 +11,15 @@ package org.mozilla.search; /** * Key should not be stored here. For more info on storing keys, see * https://github.com/ericedens/FirefoxSearch/issues/3 */ public class Constants { - public static final String POSTSEARCH_FRAGMENT = "org.mozilla.search.POSTSEARCH_FRAGMENT"; - public static final String PRESEARCH_FRAGMENT = "org.mozilla.search.PRESEARCH_FRAGMENT"; - public static final String SEARCH_FRAGMENT = "org.mozilla.search.SEARCH_FRAGMENT"; - public static final int SUGGESTION_MAX = 5; public static final String ABOUT_BLANK = "about:blank"; + + // TODO: Localize this with region.properties (or a similar solution). See bug 1065306. + public static final String DEFAULT_ENGINE_IDENTIFIER = "yahoo"; }
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java +++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java @@ -4,51 +4,58 @@ package org.mozilla.search; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; import android.support.v4.app.Fragment; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewStub; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.ImageView; import android.widget.ProgressBar; +import android.widget.TextView; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.search.providers.SearchEngine; import org.mozilla.search.providers.SearchEngineManager; public class PostSearchFragment extends Fragment { private static final String LOG_TAG = "PostSearchFragment"; private ProgressBar progressBar; private SearchEngineManager searchEngineManager; private WebView webview; + private View errorView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View mainView = inflater.inflate(R.layout.search_fragment_post_search, container, false); progressBar = (ProgressBar) mainView.findViewById(R.id.progress_bar); webview = (WebView) mainView.findViewById(R.id.webview); webview.setWebChromeClient(new ChromeClient()); - webview.setWebViewClient(new LinkInterceptingClient()); + webview.setWebViewClient(new ResultsWebViewClient()); + // This is required for our greasemonkey terror script. webview.getSettings().setJavaScriptEnabled(true); return mainView; } @Override public void onDestroyView() { @@ -72,35 +79,40 @@ public class PostSearchFragment extends searchEngineManager = null; } public void startSearch(final String query) { searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() { @Override public void execute(SearchEngine engine) { final String url = engine.resultsUriForQuery(query); - // Only load urls if the url is different than the webview's current url. - if (!TextUtils.equals(webview.getUrl(), url)) { - webview.loadUrl(Constants.ABOUT_BLANK); - webview.loadUrl(url); - } + + // Load about:blank to avoid flashing old results. + webview.loadUrl(Constants.ABOUT_BLANK); + webview.loadUrl(url); } }); } /** * A custom WebViewClient that intercepts every page load. This allows * us to decide whether to load the url here, or send it to Android - * as an intent. + * as an intent. It also handles network errors. */ - private class LinkInterceptingClient extends WebViewClient { + private class ResultsWebViewClient extends WebViewClient { + + // Whether or not there is a network error. + private boolean networkError; @Override public void onPageStarted(WebView view, final String url, Bitmap favicon) { + // Reset the error state. + networkError = false; + searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() { @Override public void execute(SearchEngine engine) { // We keep URLs in the webview that are either about:blank or a search engine result page. if (TextUtils.equals(url, Constants.ABOUT_BLANK) || engine.isSearchResultsPage(url)) { // Keeping the URL in the webview is a noop. return; } @@ -114,16 +126,50 @@ public class PostSearchFragment extends // This sends the URL directly to fennec, rather than to Android. i.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME); i.setData(Uri.parse(url)); startActivity(i); } }); } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + Log.e(LOG_TAG, "Error loading search results: " + description); + + networkError = true; + + if (errorView == null) { + final ViewStub errorViewStub = (ViewStub) getView().findViewById(R.id.error_view_stub); + errorView = errorViewStub.inflate(); + + ((ImageView) errorView.findViewById(R.id.empty_image)).setImageResource(R.drawable.network_error); + ((TextView) errorView.findViewById(R.id.empty_title)).setText(R.string.network_error_title); + + final TextView message = (TextView) errorView.findViewById(R.id.empty_message); + message.setText(R.string.network_error_message); + message.setTextColor(getResources().getColor(R.color.network_error_link)); + message.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(Settings.ACTION_SETTINGS)); + } + }); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + // Make sure the error view is hidden if the network error was fixed. + if (errorView != null) { + errorView.setVisibility(networkError ? View.VISIBLE : View.GONE); + webview.setVisibility(networkError ? View.GONE : View.VISIBLE); + } + } } /** * A custom WebChromeClient that allows us to inject CSS into * the head of the HTML and to monitor pageload progress. * * We use the WebChromeClient because it provides a hook to the titleReceived * event. Once the title is available, the page will have started parsing the
--- a/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java +++ b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java @@ -15,17 +15,19 @@ import android.support.v4.content.Cursor import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.AdapterView; +import android.widget.ImageView; import android.widget.ListView; +import android.widget.TextView; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract.SearchHistory; import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation; /** @@ -130,16 +132,21 @@ public class PreSearchFragment extends F private void updateUiFromCursor(Cursor c) { if (c != null && c.getCount() > 0) { return; } if (emptyView == null) { final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.empty_view_stub); emptyView = emptyViewStub.inflate(); + + ((ImageView) emptyView.findViewById(R.id.empty_image)).setImageResource(R.drawable.search_fox); + ((TextView) emptyView.findViewById(R.id.empty_title)).setText(R.string.search_empty_title); + ((TextView) emptyView.findViewById(R.id.empty_message)).setText(R.string.search_empty_message); + listView.setEmptyView(emptyView); } } private class SearchHistoryLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new CursorLoader(getActivity(), SEARCH_HISTORY_URI, PROJECTION, null, null,
--- a/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java +++ b/mobile/android/search/java/org/mozilla/search/SearchPreferenceActivity.java @@ -134,17 +134,17 @@ public class SearchPreferenceActivity ex entryValues[i] = engine.getIdentifier(); } final ListPreference searchEnginePref = (ListPreference) findPreference(PREF_SEARCH_ENGINE_KEY); searchEnginePref.setEntries(entries); searchEnginePref.setEntryValues(entryValues); if (searchEnginePref.getValue() == null) { - searchEnginePref.setValue(getResources().getString(R.string.default_engine_identifier)); + searchEnginePref.setValue(Constants.DEFAULT_ENGINE_IDENTIFIER); } searchEnginePref.setSummary(searchEnginePref.getEntry()); } }; task.execute(); } private void clearHistory() {
--- a/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java +++ b/mobile/android/search/java/org/mozilla/search/providers/SearchEngineManager.java @@ -9,16 +9,17 @@ import android.content.SharedPreferences import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.util.GeckoJarReader; +import org.mozilla.search.Constants; import org.mozilla.search.R; import org.mozilla.search.SearchPreferenceActivity; import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -91,26 +92,34 @@ public class SearchEngineManager impleme if (!TextUtils.isEmpty(identifier)) { try { return createEngine(identifier); } catch (IllegalArgumentException e) { Log.e(LOG_TAG, "Exception creating search engine from pref. Falling back to default engine.", e); } } - identifier = context.getResources().getString(R.string.default_engine_identifier); - return createEngine(identifier); + try { + return createEngine(Constants.DEFAULT_ENGINE_IDENTIFIER); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "Exception creating search engine from default identifier. " + + "This will happen if the locale doesn't contain the default search plugin.", e); + } + + return null; } @Override protected void onPostExecute(SearchEngine engine) { - // Only touch engine on the main thread. - SearchEngineManager.this.engine = engine; - if (callback != null) { - callback.execute(engine); + if (engine != null) { + // Only touch engine on the main thread. + SearchEngineManager.this.engine = engine; + if (callback != null) { + callback.execute(engine); + } } } }; task.execute(); } /** * Creates a list of SearchEngine instances from all available open search plugins.
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4407781b52670a5429f5bcb9090e240fc15c00ca GIT binary patch literal 5333 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ae8a0cb90e7b440457ba02eea6551f76792f373 GIT binary patch literal 4446 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
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4b0e99710ecf461969a12c936f5419352292359d GIT binary patch literal 6439 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 BSm6Kw
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0f1e2480e3aeb9067a8e497987f9845ba365fa36 GIT binary patch literal 7438 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 M07*qoM6N<$g3q8(AOHXW
--- a/mobile/android/search/res/layout/search_empty.xml +++ b/mobile/android/search/res/layout/search_empty.xml @@ -17,36 +17,35 @@ android:layout_height="0dip" android:layout_weight="1"/> <ImageView android:id="@+id/empty_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="15dp" - android:src="@drawable/search_fox" android:gravity="top|center" android:scaleType="fitCenter"/> <LinearLayout android:layout_width="match_parent" android:layout_height="0dip" android:orientation="vertical" android:layout_weight="3"> <TextView + android:id="@+id/empty_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="15dp" - android:text="@string/search_empty_title" style="@style/TextAppearance.EmptyView.Title" android:gravity="center"/> <TextView + android:id="@+id/empty_message" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/search_empty_message" style="@style/TextAppearance.EmptyView.Message" android:gravity="center"/> </LinearLayout> </LinearLayout>
--- a/mobile/android/search/res/layout/search_fragment_post_search.xml +++ b/mobile/android/search/res/layout/search_fragment_post_search.xml @@ -14,10 +14,17 @@ android:layout_width="match_parent" android:layout_height="@dimen/progress_bar_height" android:progressDrawable="@drawable/progressbar"/> <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent"/> + + <ViewStub + android:id="@+id/error_view_stub" + android:layout="@layout/search_empty" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone"/> + </LinearLayout> -
--- a/mobile/android/search/res/values/search_colors.xml +++ b/mobile/android/search/res/values/search_colors.xml @@ -20,9 +20,11 @@ <color name="widget_text_color">#5F6368</color> <!--Facet button colors--> <color name="facet_button_background_color_default">@android:color/white</color> <color name="facet_button_background_color_pressed">#FAFAFA</color> <color name="facet_button_text_color_default">#ADB0B1</color> <color name="facet_button_text_color_selected">#383E42</color> + + <color name="network_error_link">#0092DB</color> </resources>
--- a/mobile/android/search/strings/search_strings.dtd +++ b/mobile/android/search/strings/search_strings.dtd @@ -18,11 +18,10 @@ <!ENTITY pref_clearHistory_confirmation 'History cleared'> <!ENTITY pref_clearHistory_dialogMessage 'Delete all search history from this device?'> <!ENTITY pref_clearHistory_title 'Clear search history'> <!ENTITY pref_searchProvider_title 'Search engine'> <!ENTITY search_widget_button_label 'Search'> -<!-- Localization note (default_engine_identifier): Search engine identifier for the default - engine. This should be one of the identifiers listed in /searchplugins/list.txt --> -<!ENTITY default_engine_identifier 'yahoo'> +<!ENTITY network_error_title 'No internet connection'> +<!ENTITY network_error_message 'Tap here to check your network settings'>
--- a/mobile/android/search/strings/search_strings.xml.in +++ b/mobile/android/search/strings/search_strings.xml.in @@ -13,9 +13,10 @@ <string name="pref_clearHistory_dialogMessage">&pref_clearHistory_dialogMessage;</string> <string name="pref_clearHistory_title">&pref_clearHistory_title;</string> <string name="pref_searchProvider_title">&pref_searchProvider_title;</string> <string name="search_widget_name">&search_app_name;</string> <string name="search_widget_button_label">&search_widget_button_label;</string> - <string name="default_engine_identifier">&default_engine_identifier;</string> + <string name="network_error_title">&network_error_title;</string> + <string name="network_error_message">&network_error_message;</string>
--- a/services/fxaccounts/FxAccountsManager.jsm +++ b/services/fxaccounts/FxAccountsManager.jsm @@ -191,30 +191,29 @@ this.FxAccountsManager = { return this.getAccount().then( (user) => { return this._refreshAuthentication(aAudience, user.email, aPrincipal, true /* logoutOnFailure */); } ); } - } - ); - - // Otherwise, the account was deleted, so ask for Sign In/Up - return this._localSignOut().then( - () => { - return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience, - aPrincipal); - }, - (reason) => { - // reject primary problem, not signout failure - log.error("Signing out in response to server error threw: " + - reason); - return this._error(reason); + // ... otherwise, the account was deleted, so ask for Sign In/Up + return this._localSignOut().then( + () => { + return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience, + aPrincipal); + }, + (reason) => { + // reject primary problem, not signout failure + log.error("Signing out in response to server error threw: " + + reason); + return this._error(reason); + } + ); } ); } return Promise.reject(reason); }, _getAssertion: function(aAudience, aPrincipal) { return this._fxAccounts.getAssertion(aAudience).then(
--- a/storage/public/mozStorageHelper.h +++ b/storage/public/mozStorageHelper.h @@ -2,150 +2,178 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef MOZSTORAGEHELPER_H #define MOZSTORAGEHELPER_H #include "nsAutoPtr.h" +#include "nsStringGlue.h" +#include "mozilla/DebugOnly.h" #include "mozIStorageAsyncConnection.h" #include "mozIStorageConnection.h" #include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" #include "nsError.h" /** * This class wraps a transaction inside a given C++ scope, guaranteeing that * the transaction will be completed even if you have an exception or * return early. * - * aCommitOnComplete controls whether the transaction is committed or rolled - * back when it goes out of scope. A common use is to create an instance with - * commitOnComplete = FALSE (rollback), then call Commit on this object manually - * when your function completes successfully. + * A common use is to create an instance with aCommitOnComplete = false (rollback), + * then call Commit() on this object manually when your function completes + * successfully. * - * Note that nested transactions are not supported by sqlite, so if a transaction - * is already in progress, this object does nothing. Note that in this case, - * you may not get the transaction type you ask for, and you won't be able + * @note nested transactions are not supported by Sqlite, so if a transaction + * is already in progress, this object does nothing. Note that in this case, + * you may not get the transaction type you asked for, and you won't be able * to rollback. * - * Note: This class is templatized to be also usable with internal data - * structures. External users of this class should generally use - * |mozStorageTransaction| instead. + * @param aConnection + * The connection to create the transaction on. + * @param aCommitOnComplete + * Controls whether the transaction is committed or rolled back when + * this object goes out of scope. + * @param aType [optional] + * The transaction type, as defined in mozIStorageConnection. Defaults + * to TRANSACTION_DEFERRED. + * @param aAsyncCommit [optional] + * Whether commit should be executed asynchronously on the helper thread. + * This is a special option introduced as an interim solution to reduce + * main-thread fsyncs in Places. Can only be used on main-thread. + * + * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS! + * + * Notice that async commit might cause synchronous statements to fail + * with SQLITE_BUSY. A possible mitigation strategy is to use + * PRAGMA busy_timeout, but notice that might cause main-thread jank. + * Finally, if the database is using WAL journaling mode, other + * connections won't see the changes done in async committed transactions + * until commit is complete. + * + * For all of the above reasons, this should only be used as an interim + * solution and avoided completely if possible. */ -template<typename T, typename U> -class mozStorageTransactionBase +class mozStorageTransaction { public: - mozStorageTransactionBase(T* aConnection, - bool aCommitOnComplete, - int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED) + mozStorageTransaction(mozIStorageConnection* aConnection, + bool aCommitOnComplete, + int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED, + bool aAsyncCommit = false) : mConnection(aConnection), mHasTransaction(false), mCommitOnComplete(aCommitOnComplete), - mCompleted(false) + mCompleted(false), + mAsyncCommit(aAsyncCommit) { - // We won't try to get a transaction if one is already in progress. - if (mConnection) - mHasTransaction = NS_SUCCEEDED(mConnection->BeginTransactionAs(aType)); + if (mConnection) { + nsAutoCString query("BEGIN"); + switch(aType) { + case mozIStorageConnection::TRANSACTION_IMMEDIATE: + query.AppendLiteral(" IMMEDIATE"); + break; + case mozIStorageConnection::TRANSACTION_EXCLUSIVE: + query.AppendLiteral(" EXCLUSIVE"); + break; + case mozIStorageConnection::TRANSACTION_DEFERRED: + query.AppendLiteral(" DEFERRED"); + break; + default: + MOZ_ASSERT(false, "Unknown transaction type"); + } + // If a transaction is already in progress, this will fail, since Sqlite + // doesn't support nested transactions. + mHasTransaction = NS_SUCCEEDED(mConnection->ExecuteSimpleSQL(query)); + } } - ~mozStorageTransactionBase() + + ~mozStorageTransaction() { - if (mConnection && mHasTransaction && ! mCompleted) { - if (mCommitOnComplete) - mConnection->CommitTransaction(); - else - mConnection->RollbackTransaction(); + if (mConnection && mHasTransaction && !mCompleted) { + if (mCommitOnComplete) { + mozilla::DebugOnly<nsresult> rv = Commit(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), + "A transaction didn't commit correctly"); + } + else { + mozilla::DebugOnly<nsresult> rv = Rollback(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), + "A transaction didn't rollback correctly"); + } } } /** * Commits the transaction if one is in progress. If one is not in progress, * this is a NOP since the actual owner of the transaction outside of our - * scope is in charge of finally comitting or rolling back the transaction. + * scope is in charge of finally committing or rolling back the transaction. */ nsresult Commit() { - if (!mConnection || mCompleted) - return NS_OK; // no connection, or already done + if (!mConnection || mCompleted || !mHasTransaction) + return NS_OK; mCompleted = true; - if (! mHasTransaction) - return NS_OK; // transaction not ours, ignore - nsresult rv = mConnection->CommitTransaction(); + + // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle + // it, thus the transaction might stay open until the next COMMIT. + nsresult rv; + if (mAsyncCommit) { + nsCOMPtr<mozIStoragePendingStatement> ps; + rv = mConnection->ExecuteSimpleSQLAsync(NS_LITERAL_CSTRING("COMMIT"), + nullptr, getter_AddRefs(ps)); + } + else { + rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("COMMIT")); + } + if (NS_SUCCEEDED(rv)) mHasTransaction = false; return rv; } /** - * Rolls back the transaction in progress. You should only call this function - * if this object has a real transaction (HasTransaction() = true) because - * otherwise, there is no transaction to roll back. + * Rolls back the transaction if one is in progress. If one is not in progress, + * this is a NOP since the actual owner of the transaction outside of our + * scope is in charge of finally rolling back the transaction. */ nsresult Rollback() { - if (!mConnection || mCompleted) - return NS_OK; // no connection, or already done + if (!mConnection || mCompleted || !mHasTransaction) + return NS_OK; mCompleted = true; - if (! mHasTransaction) - return NS_ERROR_FAILURE; - // It is possible that a rollback will return busy, so we busy wait... + // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return + // a busy error, so this handling can be removed. nsresult rv = NS_OK; do { - rv = mConnection->RollbackTransaction(); + rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK")); if (rv == NS_ERROR_STORAGE_BUSY) (void)PR_Sleep(PR_INTERVAL_NO_WAIT); } while (rv == NS_ERROR_STORAGE_BUSY); if (NS_SUCCEEDED(rv)) mHasTransaction = false; return rv; } - /** - * Returns whether this object wraps a real transaction. False means that - * this object doesn't do anything because there was already a transaction in - * progress when it was created. - */ - bool HasTransaction() - { - return mHasTransaction; - } - - /** - * This sets the default action (commit or rollback) when this object goes - * out of scope. - */ - void SetDefaultAction(bool aCommitOnComplete) - { - mCommitOnComplete = aCommitOnComplete; - } - protected: - U mConnection; + nsCOMPtr<mozIStorageConnection> mConnection; bool mHasTransaction; bool mCommitOnComplete; bool mCompleted; + bool mAsyncCommit; }; /** - * An instance of the mozStorageTransaction<> family dedicated - * to |mozIStorageConnection|. - */ -typedef mozStorageTransactionBase<mozIStorageConnection, - nsCOMPtr<mozIStorageConnection> > -mozStorageTransaction; - - - -/** * This class wraps a statement so that it is guaraneed to be reset when * this object goes out of scope. * * Note that this always just resets the statement. If the statement doesn't * need resetting, the reset operation is inexpensive. */ class MOZ_STACK_CLASS mozStorageStatementScoper {
--- a/storage/src/mozStorageConnection.cpp +++ b/storage/src/mozStorageConnection.cpp @@ -1213,16 +1213,17 @@ Connection::initializeClone(Connection* // Copy over pragmas from the original connection. static const char * pragmas[] = { "cache_size", "temp_store", "foreign_keys", "journal_size_limit", "synchronous", "wal_autocheckpoint", + "busy_timeout" }; for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) { // Read-only connections just need cache_size and temp_store pragmas. if (aReadOnly && ::strcmp(pragmas[i], "cache_size") != 0 && ::strcmp(pragmas[i], "temp_store") != 0) { continue; }
--- a/storage/src/mozStorageConnection.h +++ b/storage/src/mozStorageConnection.h @@ -116,16 +116,24 @@ public: * @see http://sqlite.org/c3ref/commit_hook.html */ void setCommitHook(int (*aCallbackFn)(void *) , void *aData=nullptr) { MOZ_ASSERT(mDBConn, "A connection must exist at this point"); ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData); }; /** + * Gets autocommit status. + */ + bool getAutocommit() { + MOZ_ASSERT(mDBConn, "A connection must exist at this point"); + return static_cast<bool>(::sqlite3_get_autocommit(mDBConn)); + }; + + /** * Lazily creates and returns a background execution thread. In the future, * the thread may be re-claimed if left idle, so you should call this * method just before you dispatch and not save the reference. * * @returns an event target suitable for asynchronous statement execution. */ nsIEventTarget *getAsyncExecutionTarget();
--- a/storage/test/storage_test_harness.h +++ b/storage/test/storage_test_harness.h @@ -1,29 +1,36 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "TestHarness.h" + #include "nsMemory.h" +#include "prthread.h" #include "nsThreadUtils.h" #include "nsDirectoryServiceDefs.h" +#include "mozilla/ReentrantMonitor.h" + #include "mozIStorageService.h" #include "mozIStorageConnection.h" #include "mozIStorageStatementCallback.h" #include "mozIStorageCompletionCallback.h" #include "mozIStorageBindingParamsArray.h" #include "mozIStorageBindingParams.h" #include "mozIStorageAsyncStatement.h" #include "mozIStorageStatement.h" #include "mozIStoragePendingStatement.h" #include "mozIStorageError.h" -#include "nsThreadUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" static int gTotalTests = 0; static int gPassedTests = 0; #define do_check_true(aCondition) \ PR_BEGIN_MACRO \ gTotalTests++; \ if (aCondition) { \ @@ -48,37 +55,37 @@ static int gPassedTests = 0; #ifdef LINUX // XXX Linux opt builds on tinderbox are orange due to linking with stdlib. // This is sad and annoying, but it's a workaround that works. #define do_check_eq(aExpected, aActual) \ do_check_true(aExpected == aActual) #else #include <sstream> - // Print nsresult as uint32_t std::ostream& operator<<(std::ostream& aStream, const nsresult aInput) { return aStream << static_cast<uint32_t>(aInput); } - #define do_check_eq(aExpected, aActual) \ PR_BEGIN_MACRO \ gTotalTests++; \ if (aExpected == aActual) { \ gPassedTests++; \ } else { \ std::ostringstream temp; \ temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \ temp << aActual <<"' at line " << __LINE__; \ fail(temp.str().c_str()); \ } \ PR_END_MACRO #endif +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + already_AddRefed<mozIStorageService> getService() { nsCOMPtr<mozIStorageService> ss = do_GetService("@mozilla.org/storage/service;1"); do_check_true(ss); return ss.forget(); } @@ -219,8 +226,164 @@ blocking_async_execute(mozIStorageBaseSt void blocking_async_close(mozIStorageConnection *db) { nsRefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); db->AsyncClose(spinner); spinner->SpinUntilCompleted(); } + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread *watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +PRThread *last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) +{ + PRThread *curThread = ::PR_GetCurrentThread(); + if (curThread == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = curThread; + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) +{ + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + +void hook_sqlite_mutex() +{ + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() +{ + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public nsRunnable +{ +public: + explicit ThreadWedger(nsIEventTarget *aTarget) + : mReentrantMonitor("thread wedger") + , unwedged(false) + { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) + automon.Wait(); + + return NS_OK; + } + + void unwedge() + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + +private: + mozilla::ReentrantMonitor mReentrantMonitor; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed<nsIThread> +get_conn_async_thread(mozIStorageConnection *db) +{ + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("SELECT 1"), + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr<nsIThreadManager> threadMan = + do_GetService("@mozilla.org/thread-manager;1"); + nsCOMPtr<nsIThread> asyncThread; + threadMan->GetThreadFromPRThread(last_non_watched_thread, + getter_AddRefs(asyncThread)); + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr<nsIEventTarget> target = do_GetInterface(db); + nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target); + PRThread *allegedPRThread; + (void)allegedAsyncThread->GetPRThread(&allegedPRThread); + do_check_eq(allegedPRThread, last_non_watched_thread); + return asyncThread.forget(); +}
--- a/storage/test/test_transaction_helper.cpp +++ b/storage/test/test_transaction_helper.cpp @@ -2,181 +2,174 @@ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "storage_test_harness.h" #include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast<Connection *>(aDB)->getAutocommit()); +} /** * This file test our Transaction helper in mozStorageHelper.h. */ void -test_HasTransaction() -{ - nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); - - // First test that it holds the transaction after it should have gotten one. - { - mozStorageTransaction transaction(db, false); - do_check_true(transaction.HasTransaction()); - (void)transaction.Commit(); - // And that it does not have a transaction after we have committed. - do_check_false(transaction.HasTransaction()); - } - - // Check that no transaction is had after a rollback. - { - mozStorageTransaction transaction(db, false); - do_check_true(transaction.HasTransaction()); - (void)transaction.Rollback(); - do_check_false(transaction.HasTransaction()); - } - - // Check that we do not have a transaction if one is already obtained. - mozStorageTransaction outerTransaction(db, false); - do_check_true(outerTransaction.HasTransaction()); - { - mozStorageTransaction innerTransaction(db, false); - do_check_false(innerTransaction.HasTransaction()); - } -} - -void test_Commit() { nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); // Create a table in a transaction, call Commit, and make sure that it does // exists after the transaction falls out of scope. { mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); (void)transaction.Commit(); } + do_check_false(has_transaction(db)); bool exists = false; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); do_check_true(exists); } void test_Rollback() { nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); // Create a table in a transaction, call Rollback, and make sure that it does // not exists after the transaction falls out of scope. { mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); (void)transaction.Rollback(); } + do_check_false(has_transaction(db)); bool exists = true; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); do_check_false(exists); } void test_AutoCommit() { nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); // Create a table in a transaction, and make sure that it exists after the // transaction falls out of scope. This means the Commit was successful. { mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); } + do_check_false(has_transaction(db)); bool exists = false; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); do_check_true(exists); } void test_AutoRollback() { nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); // Create a table in a transaction, and make sure that it does not exists // after the transaction falls out of scope. This means the Rollback was // successful. { mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); } + do_check_false(has_transaction(db)); bool exists = true; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); do_check_false(exists); } void -test_SetDefaultAction() -{ - nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); - - // First we test that rollback happens when we first set it to automatically - // commit. - { - mozStorageTransaction transaction(db, true); - (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( - "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" - )); - transaction.SetDefaultAction(false); - } - bool exists = true; - (void)db->TableExists(NS_LITERAL_CSTRING("test1"), &exists); - do_check_false(exists); - - // Now we do the opposite and test that a commit happens when we first set it - // to automatically rollback. - { - mozStorageTransaction transaction(db, false); - (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( - "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" - )); - transaction.SetDefaultAction(true); - } - exists = false; - (void)db->TableExists(NS_LITERAL_CSTRING("test2"), &exists); - do_check_true(exists); -} - -void test_null_database_connection() { // We permit the use of the Transaction helper when passing a null database // in, so we need to make sure this still works without crashing. mozStorageTransaction transaction(nullptr, false); - - do_check_false(transaction.HasTransaction()); do_check_true(NS_SUCCEEDED(transaction.Commit())); do_check_true(NS_SUCCEEDED(transaction.Rollback())); } +void +test_async_Commit() +{ + // note this will be active for any following test. + hook_sqlite_mutex(); + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + nsRefPtr<ThreadWedger> wedger (new ThreadWedger(target)); + + { + mozStorageTransaction transaction(db, false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); + + blocking_async_close(db); +} + void (*gTests[])(void) = { - test_HasTransaction, test_Commit, test_Rollback, test_AutoCommit, test_AutoRollback, - test_SetDefaultAction, test_null_database_connection, + test_async_Commit, }; const char *file = __FILE__; #define TEST_NAME "transaction helper" #define TEST_FILE file #include "storage_test_harness_tail.h"
--- a/storage/test/test_true_async.cpp +++ b/storage/test/test_true_async.cpp @@ -1,184 +1,15 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "storage_test_harness.h" -#include "prthread.h" -#include "nsIEventTarget.h" -#include "nsIInterfaceRequestorUtils.h" - -#include "sqlite3.h" - -#include "mozilla/ReentrantMonitor.h" - -using mozilla::ReentrantMonitor; -using mozilla::ReentrantMonitorAutoEnter; - -/** - * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on - * the caller (generally main) thread. We do this by decorating the sqlite - * mutex logic with our own code that checks what thread it is being invoked on - * and sets a flag if it is invoked on the main thread. We are able to easily - * decorate the SQLite mutex logic because SQLite allows us to retrieve the - * current function pointers being used and then provide a new set. - */ - -/* ===== Mutex Watching ===== */ - -sqlite3_mutex_methods orig_mutex_methods; -sqlite3_mutex_methods wrapped_mutex_methods; - -bool mutex_used_on_watched_thread = false; -PRThread *watched_thread = nullptr; -/** - * Ugly hack to let us figure out what a connection's async thread is. If we - * were MOZILLA_INTERNAL_API and linked as such we could just include - * mozStorageConnection.h and just ask Connection directly. But that turns out - * poorly. - * - * When the thread a mutex is invoked on isn't watched_thread we save it to this - * variable. - */ -PRThread *last_non_watched_thread = nullptr; - -/** - * Set a flag if the mutex is used on the thread we are watching, but always - * call the real mutex function. - */ -extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) -{ - PRThread *curThread = ::PR_GetCurrentThread(); - if (curThread == watched_thread) - mutex_used_on_watched_thread = true; - else - last_non_watched_thread = curThread; - orig_mutex_methods.xMutexEnter(mutex); -} - -extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) -{ - if (::PR_GetCurrentThread() == watched_thread) - mutex_used_on_watched_thread = true; - return orig_mutex_methods.xMutexTry(mutex); -} - - -#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) - -void hook_sqlite_mutex() -{ - // We need to initialize and teardown SQLite to get it to set up the - // default mutex handlers for us so we can steal them and wrap them. - do_check_ok(sqlite3_initialize()); - do_check_ok(sqlite3_shutdown()); - do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); - do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); - wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; - wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; - do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); -} - -/** - * Call to clear the watch state and to set the watching against this thread. - * - * Check |mutex_used_on_watched_thread| to see if the mutex has fired since - * this method was last called. Since we're talking about the current thread, - * there are no race issues to be concerned about - */ -void watch_for_mutex_use_on_this_thread() -{ - watched_thread = ::PR_GetCurrentThread(); - mutex_used_on_watched_thread = false; -} - - -//////////////////////////////////////////////////////////////////////////////// -//// Thread Wedgers - -/** - * A runnable that blocks until code on another thread invokes its unwedge - * method. By dispatching this to a thread you can ensure that no subsequent - * runnables dispatched to the thread will execute until you invoke unwedge. - * - * The wedger is self-dispatching, just construct it with its target. - */ -class ThreadWedger : public nsRunnable -{ -public: - explicit ThreadWedger(nsIEventTarget *aTarget) - : mReentrantMonitor("thread wedger") - , unwedged(false) - { - aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); - } - - NS_IMETHOD Run() - { - ReentrantMonitorAutoEnter automon(mReentrantMonitor); - - if (!unwedged) - automon.Wait(); - - return NS_OK; - } - - void unwedge() - { - ReentrantMonitorAutoEnter automon(mReentrantMonitor); - unwedged = true; - automon.Notify(); - } - -private: - ReentrantMonitor mReentrantMonitor; - bool unwedged; -}; - -//////////////////////////////////////////////////////////////////////////////// -//// Async Helpers - -/** - * A horrible hack to figure out what the connection's async thread is. By - * creating a statement and async dispatching we can tell from the mutex who - * is the async thread, PRThread style. Then we map that to an nsIThread. - */ -already_AddRefed<nsIThread> -get_conn_async_thread(mozIStorageConnection *db) -{ - // Make sure we are tracking the current thread as the watched thread - watch_for_mutex_use_on_this_thread(); - - // - statement with nothing to bind - nsCOMPtr<mozIStorageAsyncStatement> stmt; - db->CreateAsyncStatement( - NS_LITERAL_CSTRING("SELECT 1"), - getter_AddRefs(stmt)); - blocking_async_execute(stmt); - stmt->Finalize(); - - nsCOMPtr<nsIThreadManager> threadMan = - do_GetService("@mozilla.org/thread-manager;1"); - nsCOMPtr<nsIThread> asyncThread; - threadMan->GetThreadFromPRThread(last_non_watched_thread, - getter_AddRefs(asyncThread)); - - // Additionally, check that the thread we get as the background thread is the - // same one as the one we report from getInterface. - nsCOMPtr<nsIEventTarget> target = do_GetInterface(db); - nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target); - PRThread *allegedPRThread; - (void)allegedAsyncThread->GetPRThread(&allegedPRThread); - do_check_eq(allegedPRThread, last_non_watched_thread); - return asyncThread.forget(); -} - //////////////////////////////////////////////////////////////////////////////// //// Tests void test_TrueAsyncStatement() { // (only the first test needs to call this)
--- a/storage/test/unit/test_storage_connection.js +++ b/storage/test/unit/test_storage_connection.js @@ -731,16 +731,17 @@ add_task(function test_clone_copies_prag { const PRAGMAS = [ { name: "cache_size", value: 500, copied: true }, { name: "temp_store", value: 2, copied: true }, { name: "foreign_keys", value: 1, copied: true }, { name: "journal_size_limit", value: 524288, copied: true }, { name: "synchronous", value: 2, copied: true }, { name: "wal_autocheckpoint", value: 16, copied: true }, + { name: "busy_timeout", value: 50, copied: true }, { name: "ignore_check_constraints", value: 1, copied: false }, ]; let db1 = getService().openUnsharedDatabase(getTestDB()); // Sanity check initial values are different from enforced ones. PRAGMAS.forEach(function (pragma) { let stmt = db1.createStatement("PRAGMA " + pragma.name); @@ -773,16 +774,17 @@ add_task(function test_readonly_clone_co { const PRAGMAS = [ { name: "cache_size", value: 500, copied: true }, { name: "temp_store", value: 2, copied: true }, { name: "foreign_keys", value: 1, copied: false }, { name: "journal_size_limit", value: 524288, copied: false }, { name: "synchronous", value: 2, copied: false }, { name: "wal_autocheckpoint", value: 16, copied: false }, + { name: "busy_timeout", value: 50, copied: false }, { name: "ignore_check_constraints", value: 1, copied: false }, ]; let db1 = getService().openUnsharedDatabase(getTestDB()); // Sanity check initial values are different from enforced ones. PRAGMAS.forEach(function (pragma) { let stmt = db1.createStatement("PRAGMA " + pragma.name);
--- a/toolkit/components/parentalcontrols/nsIParentalControlsService.idl +++ b/toolkit/components/parentalcontrols/nsIParentalControlsService.idl @@ -6,32 +6,53 @@ #include "nsISupports.idl" interface nsIURI; interface nsIFile; interface nsIInterfaceRequestor; interface nsIArray; -[scriptable, uuid(871cf229-2b21-4f04-b24d-e08061f14815)] +[scriptable, uuid(b3585b2a-b4b3-4aa7-be92-b8ddaa6aec5f)] interface nsIParentalControlsService : nsISupports { /** + * Action types that can be blocked for users. + */ + const short DOWNLOAD = 1; // Downloading files + const short INSTALL_EXTENSION = 2; // Installing extensions + const short INSTALL_APP = 3; // Installing webapps + const short VISIT_FILE_URLS = 4; // Opening file:/// urls + const short SHARE = 5; // Sharing + const short BOOKMARK = 6; // Creating bookmarks + const short ADD_CONTACT = 7; // Add contacts to the system database + const short SET_IMAGE = 8; // Setting images as wall paper + + /** * @returns true if the current user account has parental controls * restrictions enabled. */ readonly attribute boolean parentalControlsEnabled; /** * @returns true if the current user account parental controls * restrictions include the blocking of all file downloads. */ readonly attribute boolean blockFileDownloadsEnabled; /** + * Check if the user can do the prescibed action for this uri. + * + * @param aAction Action being performed + * @param aUri The uri requesting this action + * @param aWindow The window generating this event. + */ + boolean isAllowed(in short aAction, [optional] in nsIURI aUri); + + /** * Request that blocked URI(s) be allowed through parental * control filters. Returns true if the URI was successfully * overriden. Note, may block while native UI is shown. * * @param aTarget(s) URI to be overridden. In the case of * multiple URI, the first URI in the array * should be the root URI of the site. * @param window Window that generates the event.
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp +++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceAndroid.cpp @@ -9,17 +9,17 @@ #include "nsIFile.h" NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService) nsParentalControlsService::nsParentalControlsService() : mEnabled(false) { if (mozilla::AndroidBridge::HasEnv()) { - mEnabled = mozilla::widget::android::GeckoAppShell::IsUserRestricted(); + mEnabled = mozilla::widget::android::RestrictedProfiles::IsUserRestricted(); } } nsParentalControlsService::~nsParentalControlsService() { } NS_IMETHODIMP @@ -27,17 +27,21 @@ nsParentalControlsService::GetParentalCo { *aResult = mEnabled; return NS_OK; } NS_IMETHODIMP nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult) { - return NS_ERROR_NOT_AVAILABLE; + bool res; + IsAllowed(nsIParentalControlsService::DOWNLOAD, NULL, &res); + *aResult = res; + + return NS_OK; } NS_IMETHODIMP nsParentalControlsService::GetLoggingEnabled(bool *aResult) { return NS_ERROR_NOT_AVAILABLE; } @@ -60,8 +64,35 @@ nsParentalControlsService::RequestURIOve NS_IMETHODIMP nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets, nsIInterfaceRequestor *aWindowContext, bool *_retval) { return NS_ERROR_NOT_AVAILABLE; } + +NS_IMETHODIMP +nsParentalControlsService::IsAllowed(int16_t aAction, + nsIURI *aUri, + bool *_retval) +{ + nsresult rv = NS_OK; + *_retval = true; + + if (!mEnabled) { + return rv; + } + + if (mozilla::AndroidBridge::HasEnv()) { + nsAutoCString url; + if (aUri) { + rv = aUri->GetSpec(url); + NS_ENSURE_SUCCESS(rv, rv); + } + + *_retval = mozilla::widget::android::RestrictedProfiles::IsAllowed(aAction, + NS_ConvertUTF8toUTF16(url)); + return rv; + } + + return NS_ERROR_NOT_AVAILABLE; +}
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm +++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceCocoa.mm @@ -60,8 +60,17 @@ nsParentalControlsService::RequestURIOve NS_IMETHODIMP nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets, nsIInterfaceRequestor *aWindowContext, bool *_retval) { return NS_ERROR_NOT_AVAILABLE; } + +NS_IMETHODIMP +nsParentalControlsService::IsAllowed(int16_t aAction, + nsIURI *aUri, + bool *_retval) +{ + return NS_ERROR_NOT_AVAILABLE; +} +
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp +++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceDefault.cpp @@ -55,8 +55,16 @@ nsParentalControlsService::RequestURIOve NS_IMETHODIMP nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets, nsIInterfaceRequestor *aWindowContext, bool *_retval) { return NS_ERROR_NOT_AVAILABLE; } + +NS_IMETHODIMP +nsParentalControlsService::IsAllowed(int16_t aAction, + nsIURI *aUri, + bool *_retval) +{ + return NS_ERROR_NOT_AVAILABLE; +}
--- a/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp +++ b/toolkit/components/parentalcontrols/nsParentalControlsServiceWin.cpp @@ -326,8 +326,15 @@ nsParentalControlsService::LogFileDownlo } else { EventDataDescCreate(&eventData[WPC_ARGS_FILEDOWNLOADEVENT_PATH], (const void*)fill, sizeof(fill)); } gEventWrite(mProvider, &WPCEVENT_WEB_FILEDOWNLOAD, ARRAYSIZE(eventData), eventData); } +NS_IMETHODIMP +nsParentalControlsService::IsAllowed(int16_t aAction, + nsIURI *aUri, + bool *_retval) +{ + return NS_ERROR_NOT_AVAILABLE; +}
--- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -44,16 +44,19 @@ // Maximum size for the WAL file. It should be small enough since in case of // crashes we could lose all the transactions in the file. But a too small // file could hurt performance. #define DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES 512 #define BYTES_PER_KIBIBYTE 1024 +// How much time Sqlite can wait before returning a SQLITE_BUSY error. +#define DATABASE_BUSY_TIMEOUT_MS 100 + // Old Sync GUID annotation. #define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid") // Places string bundle, contains internationalized bookmark root names. #define PLACES_BUNDLE "chrome://places/locale/places.properties" // Livemarks annotations. #define LMANNO_FEEDURI "livemark/feedURI" @@ -592,16 +595,20 @@ Database::InitSchema(bool* aDatabaseMigr // Grow places in |growthIncrementKiB| increments to limit fragmentation on disk. // By default, it's 10 MB. int32_t growthIncrementKiB = Preferences::GetInt(PREF_GROWTH_INCREMENT_KIB, 10 * BYTES_PER_KIBIBYTE); if (growthIncrementKiB > 0) { (void)mMainConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE, EmptyCString()); } + nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = "); + busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS); + (void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma); + // We use our functions during migration, so initialize them now. rv = InitFunctions(); NS_ENSURE_SUCCESS(rv, rv); // Get the database schema version. int32_t currentSchemaVersion; rv = mMainConn->GetSchemaVersion(¤tSchemaVersion); NS_ENSURE_SUCCESS(rv, rv); @@ -1205,18 +1212,16 @@ Database::MigrateV7Up() nsresult rv = mMainConn->IndexExists(NS_LITERAL_CSTRING( "moz_places_url_uniqueindex" ), &URLUniqueIndexExists); NS_ENSURE_SUCCESS(rv, rv); if (!URLUniqueIndexExists) { return NS_ERROR_FILE_CORRUPTED; } - mozStorageTransaction transaction(mMainConn, false); - // We need an index on lastModified to catch quickly last modified bookmark // title for tag container's children. This will be useful for Sync, too. bool lastModIndexExists = false; rv = mMainConn->IndexExists( NS_LITERAL_CSTRING("moz_bookmarks_itemlastmodifiedindex"), &lastModIndexExists); NS_ENSURE_SUCCESS(rv, rv); @@ -1386,25 +1391,24 @@ Database::MigrateV7Up() rv = mMainConn->TableExists(NS_LITERAL_CSTRING("moz_inputhistory"), &tableExists); NS_ENSURE_SUCCESS(rv, rv); if (!tableExists) { rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_INPUTHISTORY); NS_ENSURE_SUCCESS(rv, rv); } - return transaction.Commit(); + return NS_OK; } nsresult Database::MigrateV8Up() { MOZ_ASSERT(NS_IsMainThread()); - mozStorageTransaction transaction(mMainConn, false); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER IF EXISTS moz_historyvisits_afterinsert_v1_trigger")); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER IF EXISTS moz_historyvisits_afterdelete_v1_trigger")); NS_ENSURE_SUCCESS(rv, rv); @@ -1440,25 +1444,24 @@ Database::MigrateV8Up() "DROP INDEX IF EXISTS moz_items_annos_attributesindex")); NS_ENSURE_SUCCESS(rv, rv); // create new item annos index rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ITEMSANNOS_PLACEATTRIBUTE); NS_ENSURE_SUCCESS(rv, rv); } - return transaction.Commit(); + return NS_OK; } nsresult Database::MigrateV9Up() { MOZ_ASSERT(NS_IsMainThread()); - mozStorageTransaction transaction(mMainConn, false); // Added in Bug 488966. The last_visit_date column caches the last // visit date, this enhances SELECT performances when we // need to sort visits by visit date. // The cached value is synced by triggers on every added or removed visit. // See nsPlacesTriggers.h for details on the triggers. bool oldIndexExists = false; nsresult rv = mMainConn->IndexExists( NS_LITERAL_CSTRING("moz_places_lastvisitdateindex"), &oldIndexExists); @@ -1480,17 +1483,17 @@ Database::MigrateV9Up() rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_places SET last_visit_date = " "(SELECT MAX(visit_date) " "FROM moz_historyvisits " "WHERE place_id = moz_places.id)")); NS_ENSURE_SUCCESS(rv, rv); } - return transaction.Commit(); + return NS_OK; } nsresult Database::MigrateV10Up() { MOZ_ASSERT(NS_IsMainThread()); // LastModified is set to the same value as dateAdded on item creation.
--- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -2262,17 +2262,19 @@ nsNavHistory::RemoveObserver(nsINavHisto } // nsNavHistory::BeginUpdateBatch // See RunInBatchMode nsresult nsNavHistory::BeginUpdateBatch() { if (mBatchLevel++ == 0) { - mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false); + mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, nsINavHistoryObserver, OnBeginUpdateBatch()); } return NS_OK; } // nsNavHistory::EndUpdateBatch @@ -2327,17 +2329,19 @@ nsNavHistory::GetHistoryDisabled(bool *_ nsresult nsNavHistory::RemovePagesInternal(const nsCString& aPlaceIdsQueryString) { // Return early if there is nothing to delete. if (aPlaceIdsQueryString.IsEmpty()) return NS_OK; - mozStorageTransaction transaction(mDB->MainConn(), false); + mozStorageTransaction transaction(mDB->MainConn(), false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); // Delete all visits for the specified place ids. nsresult rv = mDB->MainConn()->ExecuteSimpleSQL( NS_LITERAL_CSTRING( "DELETE FROM moz_historyvisits WHERE place_id IN (") + aPlaceIdsQueryString + NS_LITERAL_CSTRING(")") ); @@ -2719,17 +2723,19 @@ nsNavHistory::RemoveVisitsByTimeframe(PR deletePlaceIdsQueryString.AppendInt(placeId); } } } // force a full refresh calling onEndUpdateBatch (will call Refresh()) UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers - mozStorageTransaction transaction(mDB->MainConn(), false); + mozStorageTransaction transaction(mDB->MainConn(), false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); // Delete all visits within the timeframe. nsCOMPtr<mozIStorageStatement> deleteVisitsStmt = mDB->GetStatement( "DELETE FROM moz_historyvisits " "WHERE :from_date <= visit_date AND visit_date <= :to_date" ); NS_ENSURE_STATE(deleteVisitsStmt); mozStorageStatementScoper deletevisitsScoper(deleteVisitsStmt);
--- a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js +++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js @@ -6,17 +6,18 @@ /* Bug 1017502 - Add a foreign_count column to moz_places This tests, tests the triggers that adjust the foreign_count when a bookmark is added or removed and also the maintenance task to fix wrong counts. */ const T_URI = NetUtil.newURI("https://www.mozilla.org/firefox/nightly/firstrun/"); -function* getForeignCountForURL(conn, url){ +function* getForeignCountForURL(conn, url) { + yield promiseAsyncUpdates(); let url = url instanceof Ci.nsIURI ? url.spec : url; let rows = yield conn.executeCached( "SELECT foreign_count FROM moz_places WHERE url = :t_url ", { t_url: url }); return rows[0].getResultByName("foreign_count"); } function run_test() { run_next_test(); @@ -101,9 +102,9 @@ add_task(function* add_remove_tags_test( // Check foreign count is incremented by 2 for two tags PlacesUtils.tagging.tagURI(T_URI, ["one", "two"]); Assert.equal((yield getForeignCountForURL(conn, T_URI)), 3); // Check foreign count is set to 0 when all tags are removed PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]); Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); -}); \ No newline at end of file +});
--- a/toolkit/devtools/server/actors/timeline.js +++ b/toolkit/devtools/server/actors/timeline.js @@ -23,17 +23,17 @@ */ const {Ci, Cu} = require("chrome"); const protocol = require("devtools/server/protocol"); const {method, Arg, RetVal} = protocol; const events = require("sdk/event/core"); const {setTimeout, clearTimeout} = require("sdk/timers"); -const TIMELINE_DATA_PULL_TIMEOUT = 300; +const DEFAULT_TIMELINE_DATA_PULL_TIMEOUT = 200; // ms exports.register = function(handle) { handle.addGlobalActor(TimelineActor, "timelineActor"); handle.addTabActor(TimelineActor, "timelineActor"); }; exports.unregister = function(handle) { handle.removeGlobalActor(TimelineActor); @@ -85,18 +85,19 @@ let TimelineActor = protocol.ActorClass( * At regular intervals, pop the markers from the docshell, and forward * markers if any. */ _pullTimelineData: function() { let markers = this.docshell.popProfileTimelineMarkers(); if (markers.length > 0) { events.emit(this, "markers", markers); } - this._dataPullTimeout = setTimeout(() => this._pullTimelineData(), - TIMELINE_DATA_PULL_TIMEOUT); + this._dataPullTimeout = setTimeout(() => { + this._pullTimelineData(); + }, DEFAULT_TIMELINE_DATA_PULL_TIMEOUT); }, /** * Are we recording profile markers for the current docshell (window)? */ isRecording: method(function() { return this.docshell.recordProfileTimelineMarkers; }, { @@ -109,24 +110,24 @@ let TimelineActor = protocol.ActorClass( /** * Start/stop recording profile markers. */ start: method(function() { if (!this.docshell.recordProfileTimelineMarkers) { this.docshell.recordProfileTimelineMarkers = true; this._pullTimelineData(); } - }, {oneway: true}), + }, {}), stop: method(function() { if (this.docshell.recordProfileTimelineMarkers) { this.docshell.recordProfileTimelineMarkers = false; clearTimeout(this._dataPullTimeout); } - }, {oneway: true}), + }, {}), }); exports.TimelineFront = protocol.FrontClass(TimelineActor, { initialize: function(client, {timelineActor}) { protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor}); this.manage(this); },
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm @@ -1067,29 +1067,52 @@ this.AddonRepository = { addon[INTEGER_KEY_MAP[localName]] = value; continue; } // Handle cases that aren't as simple as grabbing the text content switch (localName) { case "type": // Map AMO's type id to corresponding string + // https://github.com/mozilla/olympia/blob/master/apps/constants/base.py#L127 + // These definitions need to be updated whenever AMO adds a new type. let id = parseInt(node.getAttribute("id")); switch (id) { case 1: addon.type = "extension"; break; case 2: addon.type = "theme"; break; case 3: addon.type = "dictionary"; break; + case 4: + addon.type = "search"; + break; + case 5: + addon.type = "langpack"; + break; + case 6: + addon.type = "langpack-addon"; + break; + case 7: + addon.type = "plugin"; + break; + case 8: + addon.type = "api"; + break; + case 9: + addon.type = "lightweight-theme"; + break; + case 11: + addon.type = "webapp"; + break; default: - logger.warn("Unknown type id when parsing addon: " + id); + logger.info("Unknown type id " + id + " found when parsing response for GUID " + guid); } break; case "authors": let authorNodes = node.getElementsByTagName("author"); for (let authorNode of authorNodes) { let name = self._getDescendantTextContent(authorNode, "name"); let link = self._getDescendantTextContent(authorNode, "link"); if (name == null || link == null)
--- a/widget/android/GeneratedJNIWrappers.cpp +++ b/widget/android/GeneratedJNIWrappers.cpp @@ -50,25 +50,23 @@ jmethodID GeckoAppShell::jGetIconForExte jmethodID GeckoAppShell::jGetMessageWrapper = 0; jmethodID GeckoAppShell::jGetMimeTypeFromExtensionsWrapper = 0; jmethodID GeckoAppShell::jGetNextMessageInListWrapper = 0; jmethodID GeckoAppShell::jGetProxyForURIWrapper = 0; jmethodID GeckoAppShell::jGetScreenDepthWrapper = 0; jmethodID GeckoAppShell::jGetScreenOrientationWrapper = 0; jmethodID GeckoAppShell::jGetShowPasswordSetting = 0; jmethodID GeckoAppShell::jGetSystemColoursWrapper = 0; -jmethodID GeckoAppShell::jGetUserRestrictions = 0; jmethodID GeckoAppShell::jHandleGeckoMessageWrapper = 0; jmethodID GeckoAppShell::jHandleUncaughtException = 0; jmethodID GeckoAppShell::jHideProgressDialog = 0; jmethodID GeckoAppShell::jInitCameraWrapper = 0; jmethodID GeckoAppShell::jIsNetworkLinkKnown = 0; jmethodID GeckoAppShell::jIsNetworkLinkUp = 0; jmethodID GeckoAppShell::jIsTablet = 0; -jmethodID GeckoAppShell::jIsUserRestricted = 0; jmethodID GeckoAppShell::jKillAnyZombies = 0; jmethodID GeckoAppShell::jLoadPluginClass = 0; jmethodID GeckoAppShell::jLockScreenOrientation = 0; jmethodID GeckoAppShell::jMarkURIVisited = 0; jmethodID GeckoAppShell::jMoveTaskToBack = 0; jmethodID GeckoAppShell::jNetworkLinkType = 0; jmethodID GeckoAppShell::jNotifyDefaultPrevented = 0; jmethodID GeckoAppShell::jNotifyIME = 0; @@ -139,25 +137,23 @@ void GeckoAppShell::InitStubs(JNIEnv *jE jGetMessageWrapper = getStaticMethod("getMessage", "(II)V"); jGetMimeTypeFromExtensionsWrapper = getStaticMethod("getMimeTypeFromExtensions", "(Ljava/lang/String;)Ljava/lang/String;"); jGetNextMessageInListWrapper = getStaticMethod("getNextMessageInList", "(II)V"); jGetProxyForURIWrapper = getStaticMethod("getProxyForURI", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String;"); jGetScreenDepthWrapper = getStaticMethod("getScreenDepth", "()I"); jGetScreenOrientationWrapper = getStaticMethod("getScreenOrientation", "()S"); jGetShowPasswordSetting = getStaticMethod("getShowPasswordSetting", "()Z"); jGetSystemColoursWrapper = getStaticMethod("getSystemColors", "()[I"); - jGetUserRestrictions = getStaticMethod("getUserRestrictions", "()Ljava/lang/String;"); jHandleGeckoMessageWrapper = getStaticMethod("handleGeckoMessage", "(Lorg/mozilla/gecko/util/NativeJSContainer;)V"); jHandleUncaughtException = getStaticMethod("handleUncaughtException", "(Ljava/lang/Thread;Ljava/lang/Throwable;)V"); jHideProgressDialog = getStaticMethod("hideProgressDialog", "()V"); jInitCameraWrapper = getStaticMethod("initCamera", "(Ljava/lang/String;III)[I"); jIsNetworkLinkKnown = getStaticMethod("isNetworkLinkKnown", "()Z"); jIsNetworkLinkUp = getStaticMethod("isNetworkLinkUp", "()Z"); jIsTablet = getStaticMethod("isTablet", "()Z"); - jIsUserRestricted = getStaticMethod("isUserRestricted", "()Z"); jKillAnyZombies = getStaticMethod("killAnyZombies", "()V"); jLoadPluginClass = getStaticMethod("loadPluginClass", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Class;"); jLockScreenOrientation = getStaticMethod("lockScreenOrientation", "(I)V"); jMarkURIVisited = getStaticMethod("markUriVisited", "(Ljava/lang/String;)V"); jMoveTaskToBack = getStaticMethod("moveTaskToBack", "()V"); jNetworkLinkType = getStaticMethod("networkLinkType", "()I"); jNotifyDefaultPrevented = getStaticMethod("notifyDefaultPrevented", "(Z)V"); jNotifyIME = getStaticMethod("notifyIME", "(I)V"); @@ -782,29 +778,16 @@ jintArray GeckoAppShell::GetSystemColour } jobject temp = env->CallStaticObjectMethod(mGeckoAppShellClass, jGetSystemColoursWrapper); AndroidBridge::HandleUncaughtException(env); jintArray ret = static_cast<jintArray>(env->PopLocalFrame(temp)); return ret; } -jstring GeckoAppShell::GetUserRestrictions() { - JNIEnv *env = AndroidBridge::GetJNIEnv(); - if (env->PushLocalFrame(1) != 0) { - AndroidBridge::HandleUncaughtException(env); - MOZ_CRASH("Exception should have caused crash."); - } - - jobject temp = env->CallStaticObjectMethod(mGeckoAppShellClass, jGetUserRestrictions); - AndroidBridge::HandleUncaughtException(env); - jstring ret = static_cast<jstring>(env->PopLocalFrame(temp)); - return ret; -} - void GeckoAppShell::HandleGeckoMessageWrapper(jobject a0) { JNIEnv *env = AndroidBridge::GetJNIEnv(); if (env->PushLocalFrame(1) != 0) { AndroidBridge::HandleUncaughtException(env); MOZ_CRASH("Exception should have caused crash."); } env->CallStaticVoidMethod(mGeckoAppShellClass, jHandleGeckoMessageWrapper, a0); @@ -887,29 +870,16 @@ bool GeckoAppShell::IsTablet() { } bool temp = env->CallStaticBooleanMethod(mGeckoAppShellClass, jIsTablet); AndroidBridge::HandleUncaughtException(env); env->PopLocalFrame(nullptr); return temp; } -bool GeckoAppShell::IsUserRestricted() { - JNIEnv *env = AndroidBridge::GetJNIEnv(); - if (env->PushLocalFrame(0) != 0) { - AndroidBridge::HandleUncaughtException(env); - MOZ_CRASH("Exception should have caused crash."); - } - - bool temp = env->CallStaticBooleanMethod(mGeckoAppShellClass, jIsUserRestricted); - AndroidBridge::HandleUncaughtException(env); - env->PopLocalFrame(nullptr); - return temp; -} - void GeckoAppShell::KillAnyZombies() { JNIEnv *env = AndroidBridge::GetJNIEnv(); if (env->PushLocalFrame(0) != 0) { AndroidBridge::HandleUncaughtException(env); MOZ_CRASH("Exception should have caused crash."); } env->CallStaticVoidMethod(mGeckoAppShellClass, jKillAnyZombies); @@ -1559,16 +1529,76 @@ void GeckoJavaSampler::UnpauseJavaProfil AndroidBridge::HandleUncaughtException(env); MOZ_CRASH("Exception should have caused crash."); } env->CallStaticVoidMethod(mGeckoJavaSamplerClass, jUnpauseJavaProfiling); AndroidBridge::HandleUncaughtException(env); env->PopLocalFrame(nullptr); } +jclass RestrictedProfiles::mRestrictedProfilesClass = 0; +jmethodID RestrictedProfiles::jGetUserRestrictions = 0; +jmethodID RestrictedProfiles::jIsAllowed = 0; +jmethodID RestrictedProfiles::jIsUserRestricted = 0; +void RestrictedProfiles::InitStubs(JNIEnv *jEnv) { + initInit(); + + mRestrictedProfilesClass = getClassGlobalRef("org/mozilla/gecko/RestrictedProfiles"); + jGetUserRestrictions = getStaticMethod("getUserRestrictions", "()Ljava/lang/String;"); + jIsAllowed = getStaticMethod("isAllowed", "(ILjava/lang/String;)Z"); + jIsUserRestricted = getStaticMethod("isUserRestricted", "()Z"); +} + +RestrictedProfiles* RestrictedProfiles::Wrap(jobject obj) { + JNIEnv *env = GetJNIForThread(); + RestrictedProfiles* ret = new RestrictedProfiles(obj, env); + env->DeleteLocalRef(obj); + return ret; +} + +jstring RestrictedProfiles::GetUserRestrictions() { + JNIEnv *env = AndroidBridge::GetJNIEnv(); + if (env->PushLocalFrame(1) != 0) { + AndroidBridge::HandleUncaughtException(env); + MOZ_CRASH("Exception should have caused crash."); + } + + jobject temp = env->CallStaticObjectMethod(mRestrictedProfilesClass, jGetUserRestrictions); + AndroidBridge::HandleUncaughtException(env); + jstring ret = static_cast<jstring>(env->PopLocalFrame(temp)); + return ret; +} + +bool RestrictedProfiles::IsAllowed(int32_t a0, const nsAString& a1) { + JNIEnv *env = AndroidBridge::GetJNIEnv(); + if (env->PushLocalFrame(1) != 0) { + AndroidBridge::HandleUncaughtException(env); + MOZ_CRASH("Exception should have caused crash."); + } + + jstring j1 = AndroidBridge::NewJavaString(env, a1); + + bool temp = env->CallStaticBooleanMethod(mRestrictedProfilesClass, jIsAllowed, a0, j1); + AndroidBridge::HandleUncaughtException(env); + env->PopLocalFrame(nullptr); + return temp; +} + +bool RestrictedProfiles::IsUserRestricted() { + JNIEnv *env = AndroidBridge::GetJNIEnv(); + if (env->PushLocalFrame(0) != 0) { + AndroidBridge::HandleUncaughtException(env); + MOZ_CRASH("Exception should have caused crash."); + } + + bool temp = env->CallStaticBooleanMethod(mRestrictedProfilesClass, jIsUserRestricted); + AndroidBridge::HandleUncaughtException(env); + env->PopLocalFrame(nullptr); + return temp; +} jclass SurfaceBits::mSurfaceBitsClass = 0; jmethodID SurfaceBits::jSurfaceBits = 0; jfieldID SurfaceBits::jbuffer = 0; jfieldID SurfaceBits::jformat = 0; jfieldID SurfaceBits::jheight = 0; jfieldID SurfaceBits::jwidth = 0; void SurfaceBits::InitStubs(JNIEnv *jEnv) { initInit(); @@ -2540,16 +2570,17 @@ void Clipboard::SetClipboardText(const n AndroidBridge::HandleUncaughtException(env); env->PopLocalFrame(nullptr); } void InitStubs(JNIEnv *jEnv) { GeckoAppShell::InitStubs(jEnv); JavaDomKeyLocation::InitStubs(jEnv); GeckoJavaSampler::InitStubs(jEnv); + RestrictedProfiles::InitStubs(jEnv); SurfaceBits::InitStubs(jEnv); ThumbnailHelper::InitStubs(jEnv); DisplayPortMetrics::InitStubs(jEnv); GLController::InitStubs(jEnv); GeckoLayerClient::InitStubs(jEnv); ImmutableViewportMetrics::InitStubs(jEnv); LayerView::InitStubs(jEnv); NativePanZoomController::InitStubs(jEnv);
--- a/widget/android/GeneratedJNIWrappers.h +++ b/widget/android/GeneratedJNIWrappers.h @@ -57,25 +57,23 @@ public: static void GetMessageWrapper(int32_t a0, int32_t a1); static jstring GetMimeTypeFromExtensionsWrapper(const nsAString& a0); static void GetNextMessageInListWrapper(int32_t a0, int32_t a1); static jstring GetProxyForURIWrapper(const nsAString& a0, const nsAString& a1, const nsAString& a2, int32_t a3); static int32_t GetScreenDepthWrapper(); static int16_t GetScreenOrientationWrapper(); static bool GetShowPasswordSetting(); static jintArray GetSystemColoursWrapper(); - static jstring GetUserRestrictions(); static void HandleGeckoMessageWrapper(jobject a0); static void HandleUncaughtException(jobject a0, jthrowable a1); static void HideProgressDialog(); static jintArray InitCameraWrapper(const nsAString& a0, int32_t a1, int32_t a2, int32_t a3); static bool IsNetworkLinkKnown(); static bool IsNetworkLinkUp(); static bool IsTablet(); - static bool IsUserRestricted(); static void KillAnyZombies(); static jclass LoadPluginClass(const nsAString& a0, const nsAString& a1); static void LockScreenOrientation(int32_t a0); static void MarkURIVisited(const nsAString& a0); static void MoveTaskToBack(); static int32_t NetworkLinkType(); static void NotifyDefaultPrevented(bool a0); static void NotifyIME(int32_t a0); @@ -145,25 +143,23 @@ protected: static jmethodID jGetMessageWrapper; static jmethodID jGetMimeTypeFromExtensionsWrapper; static jmethodID jGetNextMessageInListWrapper; static jmethodID jGetProxyForURIWrapper; static jmethodID jGetScreenDepthWrapper; static jmethodID jGetScreenOrientationWrapper; static jmethodID jGetShowPasswordSetting; static jmethodID jGetSystemColoursWrapper; - static jmethodID jGetUserRestrictions; static jmethodID jHandleGeckoMessageWrapper; static jmethodID jHandleUncaughtException; static jmethodID jHideProgressDialog; static jmethodID jInitCameraWrapper; static jmethodID jIsNetworkLinkKnown; static jmethodID jIsNetworkLinkUp; static jmethodID jIsTablet; - static jmethodID jIsUserRestricted; static jmethodID jKillAnyZombies; static jmethodID jLoadPluginClass; static jmethodID jLockScreenOrientation; static jmethodID jMarkURIVisited; static jmethodID jMoveTaskToBack; static jmethodID jNetworkLinkType; static jmethodID jNotifyDefaultPrevented; static jmethodID jNotifyIME; @@ -241,16 +237,32 @@ protected: static jmethodID jGetSampleTimeJavaProfiling; static jmethodID jGetThreadNameJavaProfilingWrapper; static jmethodID jPauseJavaProfiling; static jmethodID jStartJavaProfiling; static jmethodID jStopJavaProfiling; static jmethodID jUnpauseJavaProfiling; }; +class RestrictedProfiles : public AutoGlobalWrappedJavaObject { +public: + static void InitStubs(JNIEnv *jEnv); + static RestrictedProfiles* Wrap(jobject obj); + RestrictedProfiles(jobject obj, JNIEnv* env) : AutoGlobalWrappedJavaObject(obj, env) {}; + static jstring GetUserRestrictions(); + static bool IsAllowed(int32_t a0, const nsAString& a1); + static bool IsUserRestricted(); + RestrictedProfiles() : AutoGlobalWrappedJavaObject() {}; +protected: + static jclass mRestrictedProfilesClass; + static jmethodID jGetUserRestrictions; + static jmethodID jIsAllowed; + static jmethodID jIsUserRestricted; +}; + class SurfaceBits : public AutoGlobalWrappedJavaObject { public: static void InitStubs(JNIEnv *jEnv); static SurfaceBits* Wrap(jobject obj); SurfaceBits(jobject obj, JNIEnv* env) : AutoGlobalWrappedJavaObject(obj, env) {}; SurfaceBits(); jobject getbuffer(); void setbuffer(jobject a0);