зеркало из https://github.com/mozilla/gecko-dev.git
Коммит
eb14ebf16d
|
@ -15,7 +15,7 @@
|
|||
<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"/>
|
||||
|
@ -38,7 +38,7 @@
|
|||
<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"/>
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
<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"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</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"/>
|
||||
|
@ -130,7 +130,7 @@
|
|||
<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"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<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"/>
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
<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"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<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"/>
|
||||
|
@ -132,7 +132,7 @@
|
|||
<!-- 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"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</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"/>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"remote": "",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "a4a76a4221d7d963d01377f38d68768d0e829017",
|
||||
"revision": "6465db9982731ec95ad344901af20086ad94291f",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
<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"/>
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
<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"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</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"/>
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
<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"/>
|
||||
|
|
|
@ -1370,8 +1370,9 @@ 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);
|
||||
|
|
|
@ -2,6 +2,15 @@
|
|||
# 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",
|
||||
|
@ -122,7 +131,9 @@ let gSyncUI = {
|
|||
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;
|
||||
|
@ -275,7 +286,14 @@ let gSyncUI = {
|
|||
|
||||
// Commands
|
||||
doSync: function SUI_doSync() {
|
||||
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() {
|
||||
|
|
|
@ -1,84 +1,84 @@
|
|||
function test() {
|
||||
waitForExplicitFinish();
|
||||
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
|
||||
testLink(0, true, false, function() {
|
||||
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
|
||||
testLink(1, true, true, function() {
|
||||
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
|
||||
testLink(2, true, true, function() {
|
||||
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
|
||||
testLink(2, false, false, function() {
|
||||
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"
|
||||
testLink(3, true, true, function() {
|
||||
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
|
||||
testLink(0, true, false, function() {
|
||||
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
|
||||
testLink(4, true, false, function() {
|
||||
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>
|
||||
testLink(5, true, false, function() {
|
||||
yield testLink(5, true, false);
|
||||
|
||||
// Pinned: Link to an about: URI should not open a new tab
|
||||
// Tests link to about:mozilla
|
||||
testLink(6, true, false, finish);
|
||||
yield testLink(6, true, false);
|
||||
});
|
||||
});
|
||||
}, 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});
|
||||
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;
|
||||
|
||||
waitForDocLoadComplete(appTab.linkedBrowser).then(function() {
|
||||
let browser = gBrowser.getBrowserForTab(appTab);
|
||||
yield waitForDocLoadComplete();
|
||||
|
||||
let browser = appTab.linkedBrowser;
|
||||
if (testSubFrame)
|
||||
browser = browser.contentDocument.getElementsByTagName("iframe")[0];
|
||||
browser = browser.contentDocument.querySelector("iframe");
|
||||
|
||||
let links = browser.contentDocument.getElementsByTagName("a");
|
||||
let link = browser.contentDocument.querySelectorAll("a")[aLinkIndex];
|
||||
|
||||
let promise;
|
||||
if (expectNewTab)
|
||||
gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
|
||||
promise = waitForTabOpen();
|
||||
else
|
||||
waitForDocLoadComplete(appTab.linkedBrowser).then(onPageLoad);
|
||||
promise = waitForPageLoad(browser, link.href);
|
||||
|
||||
info("Clicking " + links[aLinkIndex].textContent);
|
||||
EventUtils.sendMouseEvent({type:"click"}, links[aLinkIndex], browser.contentWindow);
|
||||
let linkLocation = links[aLinkIndex].href;
|
||||
info("Clicking " + link.textContent);
|
||||
link.click();
|
||||
|
||||
yield promise;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -108,6 +108,19 @@ function promiseWaitForCondition(aConditionFn) {
|
|||
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);
|
||||
|
@ -443,6 +456,7 @@ function waitForDocLoadComplete(aBrowser=gBrowser) {
|
|||
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");
|
||||
|
|
|
@ -121,6 +121,23 @@ function injectLoopAPI(targetWindow) {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -338,8 +355,9 @@ function injectLoopAPI(targetWindow) {
|
|||
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));
|
||||
|
@ -347,6 +365,14 @@ function injectLoopAPI(targetWindow) {
|
|||
}
|
||||
},
|
||||
|
||||
LOOP_SESSION_TYPE: {
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
value: function() {
|
||||
return LOOP_SESSION_TYPE;
|
||||
},
|
||||
},
|
||||
|
||||
logInToFxA: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
|
|
|
@ -15,6 +15,11 @@ const INVALID_AUTH_TOKEN = 110;
|
|||
// 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");
|
||||
|
@ -22,7 +27,7 @@ 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");
|
||||
|
@ -81,6 +86,8 @@ let gErrors = new Map();
|
|||
* and register with the Loop server.
|
||||
*/
|
||||
let MozLoopServiceInternal = {
|
||||
callsData: {data: undefined},
|
||||
|
||||
// The uri of the Loop server.
|
||||
get loopServerUri() Services.prefs.getCharPref("loop.server"),
|
||||
|
||||
|
@ -202,6 +209,8 @@ let MozLoopServiceInternal = {
|
|||
/**
|
||||
* 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
|
||||
|
@ -212,14 +221,14 @@ let MozLoopServiceInternal = {
|
|||
* 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
|
||||
}
|
||||
|
@ -237,19 +246,37 @@ let MozLoopServiceInternal = {
|
|||
});
|
||||
},
|
||||
|
||||
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");
|
||||
|
@ -274,28 +301,39 @@ let MozLoopServiceInternal = {
|
|||
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.
|
||||
|
@ -307,16 +345,16 @@ let MozLoopServiceInternal = {
|
|||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
@ -337,9 +375,23 @@ let MozLoopServiceInternal = {
|
|||
// bug 1046039 for background.
|
||||
Services.prefs.setCharPref("loop.seenToS", "seen");
|
||||
|
||||
/* 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -509,7 +561,7 @@ let MozLoopServiceInternal = {
|
|||
* @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);
|
||||
});
|
||||
},
|
||||
|
@ -587,7 +639,7 @@ let MozLoopServiceInternal = {
|
|||
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);
|
||||
});
|
||||
},
|
||||
|
@ -841,6 +893,19 @@ this.MozLoopService = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.".
|
||||
*
|
||||
|
@ -921,6 +986,15 @@ this.MozLoopService = {
|
|||
}).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;
|
||||
|
@ -931,6 +1005,8 @@ this.MozLoopService = {
|
|||
/**
|
||||
* 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
|
||||
|
@ -941,8 +1017,8 @@ this.MozLoopService = {
|
|||
* 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);
|
||||
|
|
|
@ -104,7 +104,6 @@ loop.Client = (function($) {
|
|||
* -- 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)
|
||||
*/
|
||||
|
@ -188,39 +187,6 @@ loop.Client = (function($) {
|
|||
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;
|
||||
|
|
|
@ -157,7 +157,7 @@ loop.conversation = (function(OT, mozL10n) {
|
|||
*/
|
||||
var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({
|
||||
routes: {
|
||||
"incoming/:version": "incoming",
|
||||
"incoming/:callId": "incoming",
|
||||
"call/accept": "accept",
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
|
@ -182,12 +182,11 @@ loop.conversation = (function(OT, mozL10n) {
|
|||
/**
|
||||
* 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));
|
||||
|
@ -201,24 +200,16 @@ loop.conversation = (function(OT, mozL10n) {
|
|||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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._conversation.setIncomingSessionData(callData);
|
||||
this._setupWebSocketAndCallView();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -157,7 +157,7 @@ loop.conversation = (function(OT, mozL10n) {
|
|||
*/
|
||||
var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({
|
||||
routes: {
|
||||
"incoming/:version": "incoming",
|
||||
"incoming/:callId": "incoming",
|
||||
"call/accept": "accept",
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
|
@ -182,12 +182,11 @@ loop.conversation = (function(OT, mozL10n) {
|
|||
/**
|
||||
* 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));
|
||||
|
@ -201,24 +200,16 @@ loop.conversation = (function(OT, mozL10n) {
|
|||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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._conversation.setIncomingSessionData(callData);
|
||||
this._setupWebSocketAndCallView();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,10 +18,7 @@ loop.shared.models = (function(l10n) {
|
|||
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
|
||||
|
|
|
@ -182,52 +182,5 @@ describe("loop.Client", function() {
|
|||
}));
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,6 +32,7 @@ describe("loop.conversation", function() {
|
|||
setLoopCharPref: sandbox.stub(),
|
||||
getLoopCharPref: sandbox.stub(),
|
||||
getLoopBoolPref: sandbox.stub(),
|
||||
getCallData: sandbox.stub(),
|
||||
startAlerting: function() {},
|
||||
stopAlerting: function() {},
|
||||
ensureRegistered: function() {},
|
||||
|
@ -112,7 +113,6 @@ describe("loop.conversation", function() {
|
|||
sdk: {},
|
||||
pendingCallTimeout: 1000,
|
||||
});
|
||||
sandbox.stub(client, "requestCallsInfo");
|
||||
sandbox.spy(conversation, "setIncomingSessionData");
|
||||
sandbox.stub(conversation, "setOutgoingSessionData");
|
||||
});
|
||||
|
@ -157,31 +157,15 @@ describe("loop.conversation", function() {
|
|||
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() {
|
||||
|
@ -197,7 +181,7 @@ describe("loop.conversation", function() {
|
|||
|
||||
sandbox.stub(router, "_setupWebSocketAndCallView");
|
||||
|
||||
client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
|
||||
navigator.mozLoop.getCallData.returns(fakeSessionData);
|
||||
});
|
||||
|
||||
it("should store the session data", function() {
|
||||
|
|
|
@ -7,8 +7,13 @@
|
|||
|
||||
"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);
|
||||
|
@ -18,6 +23,8 @@ add_task(function* setup() {
|
|||
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));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -161,10 +168,27 @@ add_task(function* basicAuthorizationAndRegistration() {
|
|||
};
|
||||
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() {
|
||||
|
@ -178,6 +202,7 @@ add_task(function* loginWithParams401() {
|
|||
test_error: "params_401",
|
||||
};
|
||||
yield promiseOAuthParamsSetup(BASE_URL, params);
|
||||
yield MozLoopService.register(mockPushHandler);
|
||||
|
||||
let loginPromise = MozLoopService.logInToFxA();
|
||||
yield loginPromise.then(tokenData => {
|
||||
|
|
|
@ -112,3 +112,46 @@ function promiseDeletedOAuthParams(baseURL) {
|
|||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"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");
|
||||
|
||||
|
@ -17,7 +18,7 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
|||
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":
|
||||
|
@ -29,6 +30,12 @@ function handleRequest(request, response) {
|
|||
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");
|
||||
}
|
||||
|
@ -47,6 +54,7 @@ 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;
|
||||
}
|
||||
|
@ -141,3 +149,30 @@ function token(request, response) {
|
|||
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"));
|
||||
}
|
||||
|
|
|
@ -27,6 +27,13 @@ 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();
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
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_get_do_not_disturb() {
|
||||
Services.prefs.setBoolPref("loop.do_not_disturb", false);
|
||||
|
||||
|
@ -38,10 +39,15 @@ add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Chat",
|
|||
|
||||
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");
|
||||
|
||||
|
@ -16,6 +18,9 @@ add_test(function test_openChatWindow_on_notification() {
|
|||
opened = true;
|
||||
};
|
||||
|
||||
let savedHawkClient = loopServiceModule.gHawkClient;
|
||||
loopServiceModule.gHawkClient = {request: hawkGetCallsRequest};
|
||||
|
||||
mockPushHandler.notify(1);
|
||||
|
||||
do_check_true(opened, "should open a chat window");
|
||||
|
@ -23,6 +28,8 @@ add_test(function test_openChatWindow_on_notification() {
|
|||
do_check_eq(Services.prefs.getCharPref("loop.seenToS"), "seen",
|
||||
"should set the pref to 'seen'");
|
||||
|
||||
loopServiceModule.gHawkClient = savedHawkClient;
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,14 +60,8 @@ var gMainPane = {
|
|||
|
||||
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);
|
||||
|
@ -75,7 +69,6 @@ var gMainPane = {
|
|||
let showTabsInTaskbar = document.getElementById("showTabsInTaskbar");
|
||||
showTabsInTaskbar.hidden = ver < 6.1;
|
||||
} catch (ex) {}
|
||||
|
||||
#endif
|
||||
|
||||
setEventListener("browser.privatebrowsing.autostart", "change",
|
||||
|
@ -94,6 +87,11 @@ var gMainPane = {
|
|||
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
|
||||
|
|
|
@ -119,3 +119,5 @@ browser.jar:
|
|||
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)
|
||||
|
|
|
@ -31,25 +31,28 @@ loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shaderedito
|
|||
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, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
|
||||
loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel);
|
||||
loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
|
||||
|
||||
// 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 scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
|
||||
const storageProps = "chrome://browser/locale/devtools/storage.properties";
|
||||
const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.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));
|
||||
|
@ -57,10 +60,10 @@ loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBund
|
|||
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, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
|
||||
loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps));
|
||||
loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
|
||||
|
||||
let Tools = {};
|
||||
exports.Tools = Tools;
|
||||
|
@ -78,9 +81,11 @@ Tools.options = {
|
|||
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);
|
||||
}
|
||||
|
@ -113,6 +118,7 @@ Tools.webConsole = {
|
|||
isTargetSupported: function(target) {
|
||||
return true;
|
||||
},
|
||||
|
||||
build: function(iframeWindow, toolbox) {
|
||||
return new WebConsolePanel(iframeWindow, toolbox);
|
||||
}
|
||||
|
@ -230,11 +236,13 @@ Tools.canvasDebugger = {
|
|||
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);
|
||||
}
|
||||
|
@ -250,9 +258,11 @@ Tools.webAudioEditor = {
|
|||
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);
|
||||
}
|
||||
|
@ -284,11 +294,32 @@ Tools.jsprofiler = {
|
|||
}
|
||||
};
|
||||
|
||||
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",
|
||||
|
@ -312,7 +343,7 @@ Tools.netMonitor = {
|
|||
Tools.storage = {
|
||||
id: "storage",
|
||||
key: l10n("storage.commandkey", storageStrings),
|
||||
ordinal: 9,
|
||||
ordinal: 10,
|
||||
accesskey: l10n("storage.accesskey", storageStrings),
|
||||
modifiers: "shift",
|
||||
visibilityswitch: "devtools.storage.enabled",
|
||||
|
@ -337,7 +368,7 @@ Tools.storage = {
|
|||
|
||||
Tools.scratchpad = {
|
||||
id: "scratchpad",
|
||||
ordinal: 10,
|
||||
ordinal: 11,
|
||||
visibilityswitch: "devtools.scratchpad.enabled",
|
||||
icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
|
||||
invertIconForLightTheme: true,
|
||||
|
@ -367,6 +398,7 @@ let defaultTools = [
|
|||
Tools.canvasDebugger,
|
||||
Tools.webAudioEditor,
|
||||
Tools.jsprofiler,
|
||||
Tools.timeline,
|
||||
Tools.netMonitor,
|
||||
Tools.storage,
|
||||
Tools.scratchpad
|
||||
|
|
|
@ -13,11 +13,11 @@ DIRS += [
|
|||
'fontinspector',
|
||||
'framework',
|
||||
'inspector',
|
||||
'projecteditor',
|
||||
'layoutview',
|
||||
'markupview',
|
||||
'netmonitor',
|
||||
'profiler',
|
||||
'projecteditor',
|
||||
'responsivedesign',
|
||||
'scratchpad',
|
||||
'shadereditor',
|
||||
|
@ -27,6 +27,7 @@ DIRS += [
|
|||
'styleeditor',
|
||||
'styleinspector',
|
||||
'tilt',
|
||||
'timeline',
|
||||
'webaudioeditor',
|
||||
'webconsole',
|
||||
'webide',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
s/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,8 @@ support-files =
|
|||
[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]
|
||||
|
|
|
@ -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 });
|
||||
}
|
|
@ -10,7 +10,12 @@ 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";
|
||||
|
@ -494,6 +499,12 @@ AbstractCanvasGraph.prototype = {
|
|||
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.
|
||||
|
@ -955,6 +966,9 @@ AbstractCanvasGraph.prototype = {
|
|||
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");
|
||||
|
@ -990,6 +1004,9 @@ AbstractCanvasGraph.prototype = {
|
|||
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) {
|
||||
|
|
|
@ -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']
|
|
@ -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;
|
||||
})
|
||||
};
|
|
@ -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]
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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.">
|
|
@ -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
|
|
@ -58,6 +58,8 @@
|
|||
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)
|
||||
|
|
|
@ -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
|
|
@ -247,6 +247,7 @@ browser.jar:
|
|||
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)
|
||||
|
|
|
@ -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
|
|
@ -374,6 +374,7 @@ browser.jar:
|
|||
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)
|
||||
|
|
|
@ -891,7 +891,13 @@ toolbarbutton[panel-multiview-anchor="true"] > .toolbarbutton-menubutton-button
|
|||
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"] {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -284,6 +284,7 @@ browser.jar:
|
|||
* 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)
|
||||
|
@ -704,6 +705,7 @@ browser.jar:
|
|||
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)
|
||||
|
|
|
@ -790,52 +790,34 @@ nsGonkCameraControl::SetPictureSizeImpl(const Size& aSize)
|
|||
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();
|
||||
}
|
||||
|
||||
|
@ -1336,9 +1318,19 @@ nsGonkCameraControl::GetSupportedSize(const Size& aSize,
|
|||
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];
|
||||
|
|
|
@ -336,6 +336,71 @@ var tests = [
|
|||
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() {
|
||||
|
|
|
@ -64,6 +64,42 @@ MozNDEFRecord::DropData()
|
|||
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,
|
||||
|
@ -76,6 +112,10 @@ MozNDEFRecord::Constructor(const GlobalObject& aGlobal,
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
if (!ValidateTNF(aOptions, aRv)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsRefPtr<MozNDEFRecord> ndefrecord = new MozNDEFRecord(aGlobal.Context(),
|
||||
win, aOptions);
|
||||
if (!ndefrecord) {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "nsWrapperCache.h"
|
||||
#include "jsapi.h"
|
||||
|
||||
#include "mozilla/dom/MozNDEFRecordBinding.h"
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
#include "jsfriendapi.h"
|
||||
#include "js/GCAPI.h"
|
||||
|
@ -53,7 +54,7 @@ public:
|
|||
const MozNDEFRecordOptions& aOptions,
|
||||
ErrorResult& aRv);
|
||||
|
||||
uint8_t Tnf() const
|
||||
TNF Tnf() const
|
||||
{
|
||||
return mTnf;
|
||||
}
|
||||
|
@ -88,7 +89,10 @@ private:
|
|||
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;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include "NfcMessageHandler.h"
|
||||
#include <binder/Parcel.h>
|
||||
#include "mozilla/dom/MozNDEFRecordBinding.h"
|
||||
#include "nsDebug.h"
|
||||
#include "NfcGonkMessage.h"
|
||||
#include "NfcOptions.h"
|
||||
|
@ -13,6 +14,7 @@
|
|||
|
||||
using namespace android;
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::dom;
|
||||
|
||||
static const char* kConfigRequest = "config";
|
||||
static const char* kGetDetailsNDEF = "getDetailsNDEF";
|
||||
|
@ -330,7 +332,7 @@ NfcMessageHandler::ReadNDEFMessage(const Parcel& aParcel, EventOptions& aOptions
|
|||
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(
|
||||
|
@ -357,7 +359,7 @@ NfcMessageHandler::WriteNDEFMessage(Parcel& aParcel, const CommandOptions& aOpti
|
|||
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;
|
||||
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
#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;
|
||||
|
|
|
@ -25,7 +25,7 @@ using namespace mozilla::ipc;
|
|||
|
||||
static const nsLiteralString SEOriginString[] = {
|
||||
NS_LITERAL_STRING("SIM"),
|
||||
NS_LITERAL_STRING("ESE"),
|
||||
NS_LITERAL_STRING("eSE"),
|
||||
NS_LITERAL_STRING("ASSD")
|
||||
};
|
||||
|
||||
|
@ -133,6 +133,7 @@ public:
|
|||
MozNDEFRecordOptions& record = *event.mRecords.Value().AppendElement();
|
||||
|
||||
record.mTnf = recordStruct.mTnf;
|
||||
MOZ_ASSERT(record.mTnf < TNF::EndGuard_);
|
||||
|
||||
if (recordStruct.mType.Length() > 0) {
|
||||
record.mType.Construct();
|
||||
|
|
|
@ -22,8 +22,6 @@ XPCOMUtils.defineLazyServiceGetter(this,
|
|||
"appsService",
|
||||
"@mozilla.org/AppsService;1",
|
||||
"nsIAppsService");
|
||||
const NFC_PEER_EVENT_READY = 0x01;
|
||||
const NFC_PEER_EVENT_LOST = 0x02;
|
||||
|
||||
/**
|
||||
* NFCTag
|
||||
|
@ -226,9 +224,8 @@ mozNfc.prototype = {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -236,9 +233,8 @@ mozNfc.prototype = {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -285,21 +281,6 @@ mozNfc.prototype = {
|
|||
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__);
|
||||
},
|
||||
|
|
|
@ -126,8 +126,9 @@ let NCI = (function() {
|
|||
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);
|
||||
|
@ -156,8 +157,9 @@ let TAG = (function() {
|
|||
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 + "]";
|
||||
|
@ -245,7 +247,18 @@ function runTests() {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -290,7 +303,7 @@ const NDEF = {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ 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");
|
||||
|
@ -18,6 +18,34 @@ function testConstructNDEF() {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ 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)})];
|
||||
|
||||
|
|
|
@ -3470,14 +3470,6 @@ RilObject.prototype = {
|
|||
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
|
||||
|
@ -3538,6 +3530,14 @@ RilObject.prototype = {
|
|||
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;
|
||||
}
|
||||
|
@ -5854,7 +5854,11 @@ RilObject.prototype[REQUEST_SIM_IO] = function REQUEST_SIM_IO(length, options) {
|
|||
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;
|
||||
}
|
||||
|
@ -12705,6 +12709,7 @@ ICCIOHelperObject.prototype = {
|
|||
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;
|
||||
|
|
|
@ -5,22 +5,24 @@
|
|||
|
||||
/* 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.
|
||||
|
@ -43,7 +45,7 @@ interface MozNDEFRecord
|
|||
};
|
||||
|
||||
dictionary MozNDEFRecordOptions {
|
||||
octet tnf = 0; // default to tnf_empty.
|
||||
TNF tnf = "empty";
|
||||
Uint8Array type;
|
||||
Uint8Array id;
|
||||
Uint8Array payload;
|
||||
|
|
|
@ -964,7 +964,7 @@ fails == 413027-3.html 413027-3-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
|
||||
|
|
|
@ -1520,13 +1520,12 @@ public class BrowserApp extends GeckoApp
|
|||
|
||||
@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
|
||||
|
@ -2557,12 +2556,12 @@ public class BrowserApp extends GeckoApp
|
|||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -104,7 +104,6 @@ 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;
|
||||
|
@ -2554,39 +2553,6 @@ public class GeckoAppShell
|
|||
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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -841,6 +841,14 @@ public class Tabs implements GeckoEventListener {
|
|||
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.
|
||||
|
|
|
@ -369,6 +369,7 @@ gbjar.sources += [
|
|||
'ReaderModeUtils.java',
|
||||
'RemoteTabsExpandableListAdapter.java',
|
||||
'Restarter.java',
|
||||
'RestrictedProfiles.java',
|
||||
'ScrollAnimator.java',
|
||||
'ServiceNotificationClient.java',
|
||||
'SessionParser.java',
|
||||
|
|
|
@ -674,7 +674,9 @@ OnSharedPreferenceChangeListener
|
|||
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--;
|
||||
|
|
|
@ -63,10 +63,7 @@ public class LocaleListPreference extends ListPreference {
|
|||
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.
|
||||
|
@ -272,8 +269,7 @@ public class LocaleListPreference extends ListPreference {
|
|||
|
||||
// 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() {
|
||||
|
|
|
@ -18,28 +18,38 @@ add_task(function test_isUserRestricted() {
|
|||
// 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();
|
||||
|
|
|
@ -652,6 +652,10 @@ var SelectionHandler = {
|
|||
},
|
||||
selector: {
|
||||
matches: function() {
|
||||
if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SelectionHandler.isSelectionActive();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,6 +164,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetErrorHelper",
|
|||
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");
|
||||
|
||||
|
@ -287,7 +290,6 @@ var BrowserApp = {
|
|||
_tabs: [],
|
||||
_selectedTab: null,
|
||||
_prefObservers: [],
|
||||
isGuest: false,
|
||||
|
||||
get isTablet() {
|
||||
let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
|
||||
|
@ -434,8 +436,6 @@ var BrowserApp = {
|
|||
gScreenHeight = window.arguments[2];
|
||||
if (window.arguments[3])
|
||||
pinned = window.arguments[3];
|
||||
if (window.arguments[4])
|
||||
this.isGuest = window.arguments[4];
|
||||
}
|
||||
|
||||
if (pinned) {
|
||||
|
@ -459,7 +459,7 @@ var BrowserApp = {
|
|||
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);
|
||||
|
@ -581,7 +581,7 @@ var BrowserApp = {
|
|||
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(),
|
||||
|
@ -597,7 +597,7 @@ var BrowserApp = {
|
|||
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);
|
||||
|
@ -616,7 +616,7 @@ var BrowserApp = {
|
|||
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);
|
||||
|
@ -633,7 +633,7 @@ var BrowserApp = {
|
|||
});
|
||||
|
||||
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");
|
||||
|
||||
|
@ -645,7 +645,7 @@ var BrowserApp = {
|
|||
});
|
||||
|
||||
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");
|
||||
|
||||
|
@ -657,7 +657,7 @@ var BrowserApp = {
|
|||
});
|
||||
|
||||
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");
|
||||
|
||||
|
@ -694,7 +694,7 @@ var BrowserApp = {
|
|||
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;
|
||||
|
@ -742,7 +742,7 @@ var BrowserApp = {
|
|||
|
||||
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;
|
||||
|
@ -774,7 +774,7 @@ var BrowserApp = {
|
|||
});
|
||||
|
||||
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");
|
||||
|
||||
|
@ -2680,11 +2680,13 @@ var NativeWindow = {
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
@ -4202,8 +4204,8 @@ Tab.prototype = {
|
|||
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) {
|
||||
|
|
|
@ -256,11 +256,12 @@ AlertDownloadProgressListener.prototype = {
|
|||
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"),
|
||||
|
|
|
@ -18,7 +18,6 @@ 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);
|
||||
|
||||
|
@ -29,13 +28,11 @@ function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) {
|
|||
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);
|
||||
}
|
||||
|
||||
|
@ -61,7 +58,6 @@ BrowserCLH.prototype = {
|
|||
handle: function fs_handle(aCmdLine) {
|
||||
let openURL = "about:home";
|
||||
let pinned = false;
|
||||
let guest = false;
|
||||
|
||||
let width = 1;
|
||||
let height = 1;
|
||||
|
@ -72,9 +68,6 @@ BrowserCLH.prototype = {
|
|||
try {
|
||||
pinned = aCmdLine.handleFlag("webapp", false);
|
||||
} catch (e) { /* Optional */ }
|
||||
try {
|
||||
guest = aCmdLine.handleFlag("guest", false);
|
||||
} catch (e) { /* Optional */ }
|
||||
|
||||
try {
|
||||
width = aCmdLine.handleFlagWithParam("width", false);
|
||||
|
@ -102,7 +95,6 @@ BrowserCLH.prototype = {
|
|||
pinned: pinned,
|
||||
width: width,
|
||||
height: height,
|
||||
guest: guest
|
||||
};
|
||||
|
||||
// Make sure webapps do not have: locationbar, personalbar, menubar, statusbar, and toolbar
|
||||
|
|
|
@ -60,4 +60,4 @@ retrievalFailedTitle=#1 update failed;#1 updates 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
|
||||
|
|
|
@ -29,6 +29,9 @@ 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
|
||||
|
@ -89,8 +92,8 @@ this.WebappManager = {
|
|||
},
|
||||
|
||||
_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;
|
||||
}
|
||||
|
@ -277,10 +280,6 @@ this.WebappManager = {
|
|||
|
||||
}),
|
||||
|
||||
inGuestSession: function() {
|
||||
return Services.wm.getMostRecentWindow("navigator:browser").BrowserApp.isGuest;
|
||||
},
|
||||
|
||||
autoInstall: function(aData) {
|
||||
debug("autoInstall " + aData.manifestURL);
|
||||
|
||||
|
|
|
@ -16,11 +16,10 @@ package org.mozilla.search;
|
|||
*/
|
||||
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";
|
||||
}
|
||||
|
|
|
@ -9,15 +9,20 @@ 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;
|
||||
|
@ -33,6 +38,7 @@ public class PostSearchFragment extends Fragment {
|
|||
|
||||
private SearchEngineManager searchEngineManager;
|
||||
private WebView webview;
|
||||
private View errorView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
|
@ -43,7 +49,8 @@ public class PostSearchFragment extends Fragment {
|
|||
|
||||
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);
|
||||
|
||||
|
@ -77,12 +84,11 @@ public class PostSearchFragment extends Fragment {
|
|||
@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)) {
|
||||
|
||||
// Load about:blank to avoid flashing old results.
|
||||
webview.loadUrl(Constants.ABOUT_BLANK);
|
||||
webview.loadUrl(url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -90,12 +96,18 @@ public class PostSearchFragment extends Fragment {
|
|||
/**
|
||||
* 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) {
|
||||
|
@ -119,6 +131,40 @@ public class PostSearchFragment extends Fragment {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче