Merge m-c to inbound. a=merge

CLOSED TREE
This commit is contained in:
Ryan VanderMeulen 2014-09-10 18:36:26 -04:00
commit eb14ebf16d
132 changed files with 3664 additions and 898 deletions

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "a4a76a4221d7d963d01377f38d68768d0e829017",
"revision": "6465db9982731ec95ad344901af20086ad94291f",
"repo_path": "/integration/gaia-central"
}

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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);

View File

@ -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() {
setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0);
let needsSetup = this._needsSetup();
let loginFailed = this._loginFailed();
if (!(loginFailed || needsSetup)) {
setTimeout(function () Weave.Service.errorHandler.syncAndReportErrors(), 0);
}
Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
},
handleToolbarButton: function SUI_handleStatusbarButton() {

View File

@ -1,84 +1,84 @@
function test() {
waitForExplicitFinish();
// Pinned: Link to the same domain should not open a new tab
// Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html
testLink(0, true, false, function() {
// Pinned: Link to a different subdomain should open a new tab
// Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html
testLink(1, true, true, function() {
// Pinned: Link to a different domain should open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
testLink(2, true, true, function() {
// Not Pinned: Link to a different domain should not open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
testLink(2, false, false, function() {
// Pinned: Targetted link should open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo"
testLink(3, true, true, function() {
// Pinned: Link in a subframe should not open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe
testLink(0, true, false, function() {
// Pinned: Link to the same domain (with www prefix) should not open a new tab
// Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html
testLink(4, true, false, function() {
// Pinned: Link to a data: URI should not open a new tab
// Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
testLink(5, true, false, function() {
// Pinned: Link to an about: URI should not open a new tab
// Tests link to about:mozilla
testLink(6, true, false, finish);
});
});
}, true);
});
});
});
});
});
}
function testLink(aLinkIndex, pinTab, expectNewTab, nextTest, testSubFrame) {
let appTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/general/app_bug575561.html", {skipAnimation: true});
if (pinTab)
gBrowser.pinTab(appTab);
gBrowser.selectedTab = appTab;
waitForDocLoadComplete(appTab.linkedBrowser).then(function() {
let browser = gBrowser.getBrowserForTab(appTab);
if (testSubFrame)
browser = browser.contentDocument.getElementsByTagName("iframe")[0];
let links = browser.contentDocument.getElementsByTagName("a");
if (expectNewTab)
gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
else
waitForDocLoadComplete(appTab.linkedBrowser).then(onPageLoad);
info("Clicking " + links[aLinkIndex].textContent);
EventUtils.sendMouseEvent({type:"click"}, links[aLinkIndex], browser.contentWindow);
let linkLocation = links[aLinkIndex].href;
function onPageLoad() {
browser.removeEventListener("load", onPageLoad, true);
is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab");
executeSoon(function(){
gBrowser.removeTab(appTab);
nextTest();
});
}
function onTabOpen(event) {
gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
ok(true, "Link should open a new tab");
waitForDocLoadComplete(event.target.linkedBrowser).then(function() {
executeSoon(function(){
gBrowser.removeTab(appTab);
gBrowser.removeCurrentTab();
nextTest();
});
});
}
});
}
const TEST_URL = "http://example.com/browser/browser/base/content/test/general/app_bug575561.html";
add_task(function*() {
SimpleTest.requestCompleteLog();
// Pinned: Link to the same domain should not open a new tab
// Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html
yield testLink(0, true, false);
// Pinned: Link to a different subdomain should open a new tab
// Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html
yield testLink(1, true, true);
// Pinned: Link to a different domain should open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
yield testLink(2, true, true);
// Not Pinned: Link to a different domain should not open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html
yield testLink(2, false, false);
// Pinned: Targetted link should open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo"
yield testLink(3, true, true);
// Pinned: Link in a subframe should not open a new tab
// Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe
yield testLink(0, true, false, true);
// Pinned: Link to the same domain (with www prefix) should not open a new tab
// Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html
yield testLink(4, true, false);
// Pinned: Link to a data: URI should not open a new tab
// Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
yield testLink(5, true, false);
// Pinned: Link to an about: URI should not open a new tab
// Tests link to about:mozilla
yield testLink(6, true, false);
});
let waitForPageLoad = Task.async(function*(browser, linkLocation) {
yield waitForDocLoadComplete();
is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab");
});
let waitForTabOpen = Task.async(function*() {
let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true);
ok(true, "Link should open a new tab");
yield waitForDocLoadComplete(event.target.linkedBrowser);
yield Promise.resolve();
gBrowser.removeCurrentTab();
});
let testLink = Task.async(function*(aLinkIndex, pinTab, expectNewTab, testSubFrame) {
let appTab = gBrowser.addTab(TEST_URL, {skipAnimation: true});
if (pinTab)
gBrowser.pinTab(appTab);
gBrowser.selectedTab = appTab;
yield waitForDocLoadComplete();
let browser = appTab.linkedBrowser;
if (testSubFrame)
browser = browser.contentDocument.querySelector("iframe");
let link = browser.contentDocument.querySelectorAll("a")[aLinkIndex];
let promise;
if (expectNewTab)
promise = waitForTabOpen();
else
promise = waitForPageLoad(browser, link.href);
info("Clicking " + link.textContent);
link.click();
yield promise;
gBrowser.removeTab(appTab);
});

View File

@ -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");

View File

@ -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,

View File

@ -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");
this.openChatWindow(null,
this.localizedStrings["incoming_call_title2"].textContent,
"about:loopconversation#incoming/" + version);
/* Request the information on the new call(s) associated with this version. */
this.hawkRequest(LOOP_SESSION_TYPE.GUEST,
"/calls?version=" + version, "GET").then(response => {
try {
let respData = JSON.parse(response.body);
if (respData.calls && respData.calls[0]) {
this.callsData.data = respData.calls[0];
this.openChatWindow(null,
this.localizedStrings["incoming_call_title2"].textContent,
"about:loopconversation#incoming/" + version);
} else {
console.warn("Error: missing calls[] in response");
}
} catch (err) {
console.warn("Error parsing calls info", err);
}
});
},
/**
@ -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);

View File

@ -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;

View File

@ -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);
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 1032700 should fix this.
this._conversation.setIncomingSessionData(sessionData[0]);
this._setupWebSocketAndCallView();
}.bind(this));
var callData = navigator.mozLoop.getCallData(callId);
if (!callData) {
console.error("Failed to get the call data");
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}
this._conversation.setIncomingSessionData(callData);
this._setupWebSocketAndCallView();
},
/**

View File

@ -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);
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}
// XXX For incoming calls we might have more than one call queued.
// For now, we'll just assume the first call is the right information.
// We'll probably really want to be getting this data from the
// background worker on the desktop client.
// Bug 1032700 should fix this.
this._conversation.setIncomingSessionData(sessionData[0]);
this._setupWebSocketAndCallView();
}.bind(this));
var callData = navigator.mozLoop.getCallData(callId);
if (!callData) {
console.error("Failed to get the call data");
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this._notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}
this._conversation.setIncomingSessionData(callData);
this._setupWebSocketAndCallView();
},
/**

View File

@ -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

View File

@ -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);
}));
});
});
});
});

View File

@ -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() {

View File

@ -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 => {

View File

@ -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);
}
};

View File

@ -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"));
}

View File

@ -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();

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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',

View File

@ -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/ */
/**

View File

@ -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]

View File

@ -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 });
}

View File

@ -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) {

View File

@ -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']

View File

@ -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;
})
};

View File

@ -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]

View File

@ -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();
});

View File

@ -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();
}

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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>

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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.">

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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"] {

View File

@ -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;
}

View File

@ -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

View File

@ -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)

View File

@ -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();
}
@ -1283,7 +1265,7 @@ nsGonkCameraControl::SetPreviewSize(const Size& aSize)
}
Size best;
rv = GetSupportedSize(aSize, previewSizes, best);
rv = GetSupportedSize(aSize, previewSizes, best);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to find a supported preview size, requested size %dx%d",
aSize.width, aSize.height);
@ -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];

View File

@ -336,10 +336,75 @@ 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() {
for (var i = 0; i < tests.length; ++i ) {
for (var i = 0; i < tests.length; ++i) {
yield tests[i];
}
}();

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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__);
},

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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)})];

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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.

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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.

View File

@ -369,6 +369,7 @@ gbjar.sources += [
'ReaderModeUtils.java',
'RemoteTabsExpandableListAdapter.java',
'Restarter.java',
'RestrictedProfiles.java',
'ScrollAnimator.java',
'ServiceNotificationClient.java',
'SessionParser.java',

View File

@ -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--;

View File

@ -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() {

View File

@ -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();

View File

@ -652,6 +652,10 @@ var SelectionHandler = {
},
selector: {
matches: function() {
if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
return false;
}
return SelectionHandler.isSelectionActive();
}
}

View File

@ -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) {

View File

@ -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"),

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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";
}

View File

@ -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,11 +84,10 @@ 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)) {
webview.loadUrl(Constants.ABOUT_BLANK);
webview.loadUrl(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);
}
}
}
/**

Some files were not shown because too many files have changed in this diff Show More