Merge autoland to mozilla-central. a=merge

This commit is contained in:
Tiberius Oros 2018-08-17 00:31:31 +03:00
commit 2010f3a375
121 changed files with 3293 additions and 2974 deletions

View file

@ -78,6 +78,8 @@ class LinkHandlerChild extends ActorChild {
if (this._iconLoader) {
this._iconLoader.onPageHide();
}
this.seenTabIcon = false;
}
onLinkEvent(event) {

View file

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<blocklist lastupdate="1533550294490" xmlns="http://www.mozilla.org/2006/addons-blocklist">
<blocklist lastupdate="1534336165428" xmlns="http://www.mozilla.org/2006/addons-blocklist">
<emItems>
<emItem blockID="i334" id="{0F827075-B026-42F3-885D-98981EE7B1AE}">
<prefs/>
@ -2308,6 +2308,18 @@
<prefs/>
<versionRange minVersion="0" maxVersion="*" severity="3"/>
</emItem>
<emItem blockID="cee5c2ab-1059-4b15-a78c-1203116552c4" id="/^(({0f9e469e-4245-43f8-a7a8-7e730f80d284})|({117ca2f3-df4c-4e17-a5c5-b49077e9c731})|({11db147a-a1cb-43dd-9c05-0d11683483e1})|({1ed2af70-9e89-42db-a9e8-17ae594003ac})|({24ed6bdc-3085-413b-a62e-dc5dd30272f4})|({2aa19a7a-2a43-4e0d-a3dc-abb33fa7e2b6})|({3d6fbbb3-6c80-47bb-af20-56fcaebcb9ca})|({42f4c194-8929-42b9-a9a3-afa56dd0913b})|({46740fa0-896d-4f2e-a240-9478865c47c2})|({4718da68-a373-4a03-a77b-0f49b8bb40ee})|({4d41e0b8-bf7e-45ab-bd90-c426b420e3ee})|({50957a38-c15d-42da-94f5-325bc74a554c})|({5650fc63-a7c5-4627-8d0a-99b20dcbd94b})|({5c5c38ec-08bf-493a-9352-6ccf25d60c08})|({67ecb446-9ccd-4193-a27f-7bd1521bd03c})|({71f01ffe-226d-4634-9b21-968f5ce9f8f5})|({72f31855-2412-4998-a6ff-978f89bba0c3})|({7b3c1e86-2599-4e1a-ad98-767ae38286c8})|({7c37463c-001e-4f58-9e88-aaab2a624551})|({7de64f18-8e6b-4c41-9b05-d8872b418026})|({82dcf841-c7e1-4764-bb47-caa28909e447})|({872f20ea-196e-4d11-8835-1cc4c877b1b8})|({8efee317-546f-418d-82d3-60cc5187acf5})|({93deeba1-0126-43f7-a94d-4eecfce53b33})|({9cc12446-16da-4200-b284-d5fc18670825})|({9cd27996-6068-4597-8e97-bb63f783a224})|({9fdcedc7-ffde-44c3-94f6-4196b1e0d9fc})|({a191563e-ac30-4c5a-af3d-85bb9e9f9286})|({a4cb0430-c92e-44c6-9427-6a6629c4c5f6})|({a87f1b9b-8817-4bff-80fd-db96020c56c8})|({ae29a313-c6a9-48be-918d-1e4c67ba642f})|({b2cea58a-845d-4394-9b02-8a31cfbb4873})|({b420e2be-df31-4bea-83f4-103fe0aa558c})|({b77afcab-0971-4c50-9486-f6f54845a273})|({b868c6f4-5841-4c14-86ee-d60bbfd1cec1})|({b99ae7b1-aabb-4674-ba8f-14ed32d04e76})|({b9bb8009-3716-4d0c-bcb4-35f9874e931e})|({c53c4cbc-04a7-4771-9e97-c08c85871e1e})|({ce0d1384-b99b-478e-850a-fa6dfbe5a2d4})|({cf8e8789-e75d-4823-939f-c49a9ae7fba2})|({d0f67c53-42b5-4650-b343-d9664c04c838})|({dfa77d38-f67b-4c41-80d5-96470d804d09})|({e20c916e-12ea-445b-b6f6-a42ec801b9f8})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({e7d03b09-24b3-4d99-8e1b-c510f5d13612})|({fa8141ba-fa56-414e-91c0-898135c74c9d})|({fc99b961-5878-46b4-b091-6d2f507bf44d})|(firedocs@mozilla\.com)|(firetasks@mozilla\.com)|(getta@mozilla\.com)|(javideo@mozilla\.com)|(javideo2@mozilla\.com)|(javideos@mozilla\.com)|(javideosz@mozilla\.com)|(search_free@mozilla\.com)|(search-unlisted@mozilla\.com)|(search-unlisted101125511@mozilla\.com)|(search-unlisted10155511@mozilla\.com)|(search-unlisted1025525511@mozilla\.com)|(search-unlisted1099120071@mozilla\.com)|(search-unlisted1099125511@mozilla\.com)|(search-unlisted109925511@mozilla\.com)|(search-unlisted11@mozilla\.com)|(search-unlisted111@mozilla\.com)|(search-unlisted12@mozilla\.com)|(search-unlisted14400770034@mozilla\.com)|(search-unlisted144007741154@mozilla\.com)|(search-unlisted144436110034@mozilla\.com)|(search-unlisted14454@mozilla\.com)|(search-unlisted1570124111@mozilla\.com)|(search-unlisted1570254441111@mozilla\.com)|(search-unlisted15721239034@mozilla\.com)|(search-unlisted157441@mozilla\.com)|(search-unlisted15757771@mozilla\.com)|(search-unlisted1577122001@mozilla\.com)|(search-unlisted15777441001@mozilla\.com)|(search-unlisted15788120036001@mozilla\.com)|(search-unlisted157881200361111@mozilla\.com)|(search-unlisted1578899961111@mozilla\.com)|(search-unlisted157999658@mozilla\.com)|(search-unlisted158436561@mozilla\.com)|(search-unlisted158440374111@mozilla\.com)|(search-unlisted15874111@mozilla\.com)|(search-unlisted1741395551@mozilla\.com)|(search-unlisted17441000051@mozilla\.com)|(search-unlisted174410000522777441@mozilla\.com)|(search-unlisted1768fdgfdg@mozilla\.com)|(search-unlisted180000411@mozilla\.com)|(search-unlisted18000411@mozilla\.com)|(search-unlisted1800411@mozilla\.com)|(search-unlisted18011888@mozilla\.com)|(search-unlisted1801668@mozilla\.com)|(search-unlisted18033411@mozilla\.com)|(search-unlisted180888@mozilla\.com)|(search-unlisted181438@mozilla\.com)|(search-unlisted18411@mozilla\.com)|(search-unlisted18922544@mozilla\.com)|(search-unlisted1955511@mozilla\.com)|(search-unlisted2@mozilla\.com)|(search-unlisted3@mozilla\.com)|(search-unlisted4@mozilla\.com)|(search-unlisted400@mozilla\.com)|(search-unlisted40110@mozilla\.com)|(search-unlisted5@mozilla\.com)|(search-unlisted55@mozilla\.com)|(search@mozilla\.com)|(searchazsd@mozilla\.com)|(smart246@mozilla\.com)|(smarter1@mozilla\.com)|(smarters1@mozilla\.com)|(stream@mozilla\.com)|(tahdith@mozilla\.com)|(therill@mozilla\.com)|(Updates@mozilla\.com))$/">
<prefs/>
<versionRange minVersion="0" maxVersion="*" severity="3"/>
</emItem>
<emItem blockID="2734325e-143b-4962-98bf-4b18c77407e2" id="/^((Timemetric@tmetric)|(textMarkertool@underFlyingBirches\.org)|(youpanel@jetpack)|({6f13489d-b274-45b6-80fa-e9daa140e1a4})|({568db771-c718-4587-bcd0-e3728ee53550})|({829827cd-03be-4fed-af96-dd5997806fb4})|({9077390b-89a9-41ad-998f-ab973e37f26f})|({8e7269ac-a171-4d9f-9c0a-c504848fd52f})|({aaaffe20-3306-4c64-9fe5-66986ebb248e})|({bf153de7-cdf2-4554-af46-29dabfb2aa2d})|({c579191c-6bb8-4795-adca-d1bf180b512d})|({e2a4966f-919d-4afc-a94f-5bd6e0606711})|({ee97f92d-1bfe-4e9d-816c-0dfcd63a6206}))$/">
<prefs/>
<versionRange minVersion="0" maxVersion="*" severity="3"/>
</emItem>
<emItem blockID="d9892a76-b22e-40bd-8073-89b0f8110ec7" id="/^(({1a3fb414-0945-405c-a62a-9fe5e1a50c69})|({1a45f6aa-d80a-4317-84d2-0ce43671b08a})|({2d52a462-8bec-4708-9cd1-894b682bdc78})|({3f841cfc-de5a-421f-8bd7-2bf1d943b02a})|({5c7601bf-522b-47e5-b0f0-ea0e706af443})|({7ebe580f-71c9-4ef8-8073-f38deaeb9dfb})|({8b2188fd-1daf-4851-b387-28d964014353})|({8cee42ac-f1fe-40ae-aed6-24e3b76b2f77})|({8d13c4a9-5e8c-47a6-b583-681c83164ac9})|({9b1d775a-1877-45c9-ad48-d6fcfa4fff39})|({9efdbe5f-6e51-4a35-a41b-71dc939e6221})|({23f63efb-156e-440b-a96c-118bebc21057})|({026dfc8c-ecc8-41ba-b45f-70ffbd5cc672})|({34aa433c-27e9-4c87-a662-9f82f99eb9af})|({36f34d69-f22f-47c3-b4cd-4f37b7676107})|({39bd8607-0af4-4d6b-bd69-9a63c1825d3c})|({48c6ad6d-297c-4074-8fef-ca5f07683859})|({54aa688d-9504-481d-ba75-cfee421b98e0})|({59f59748-e6a8-4b41-87b5-9baadd75ddef})|({61d99407-1231-4edc-acc8-ab96cbbcf151})|({68ca8e3a-397a-4135-a3af-b6e4068a1eae})|({71beafd6-779b-4b7d-a78b-18a107277b59})|({83ed90f8-b07e-4c45-ba6b-ba2fe12cebb6})|({231dfb44-98e0-4bc4-b6ee-1dac4a836b08})|({273f0bce-33f4-45f6-ae03-df67df3864c2})|({392f4252-c731-4715-9f8d-d5815f766abb})|({484ec5d0-4cfd-4d96-88d0-a349bfc33780})|({569dbf47-cc10-41c4-8fd5-5f6cf4a833c7})|({578cad7a-57d5-404d-8dda-4d30de33b0c2})|({986b2c3f-e335-4b39-b3ad-46caf809d3aa})|({1091c11f-5983-410e-a715-0968754cff54})|({2330eb8a-e3fe-4b2e-9f17-9ddbfb96e6f5})|({5920b042-0af1-4658-97c1-602315d3b93d})|({6331a47f-8aae-490c-a9ad-eae786b4349f})|({6698b988-c3ef-4e1f-8740-08d52719eab5})|({30516f71-88d4-489b-a27f-d00a63ad459f})|({12089699-5570-4bf6-890f-07e7f674aa6e})|({84887738-92bf-4903-a5e8-695fd078c657})|({8562e48e-3723-412a-9ebd-b33d3d3b29dd})|({6e449795-c545-41be-92c0-5d467c147389})|({1e369c7c-6b61-436e-8978-4640687670d6})|({a03d427a-bd2e-42b6-828f-a57f38fac7b5})|({a77fc9b9-6ebb-418d-b0b6-86311c191158})|({a368025b-9828-43a1-8a5c-f6fab61c9be9})|({b1908b02-410d-4778-8856-7e259fbf471d})|({b9425ace-c2e9-4ec4-b564-4062546f4eca})|({b9845b5d-70c9-419c-a9a5-98ea8ee5cc01})|({ba99fee7-9806-4e32-8257-a33ffc3b8539})|({bdf8767d-ae4c-4d45-8f95-0ba29b910600})|({c6c4a718-cf91-4648-aa9b-170d66163cf2})|({ca0f2988-e1a8-4e83-afde-0dca56a17d5f})|({cac5db09-979b-40e3-8c8e-d96397b0eecb})|({d3b5280b-f8d8-4669-bdf6-91f23ae58042})|({d73d2f6a-ea24-4b1b-8c76-563fce9f786d})|({d77fed37-85c0-4b94-89bb-0d2849472b8d})|({d371abec-84bb-481b-acbf-235639451127})|({de47a3b4-dad1-4f4a-bdd6-8666586e29e8})|({ded6afad-2aaa-446b-b6bd-b12a8a61c945})|({e0c3a1ca-8e21-4d1b-b53b-ea115cf59172})|({e6bbf496-6489-4b48-8e5a-799aad4aa742})|({e63b262a-f9b8-4496-9c4b-9d3cbd6aea90})|({e73c1b5d-20f7-4d86-ad16-9de3c27718e2})|({eb01dc49-688f-4a21-aa8d-49bd88a8f319})|({edc9816b-60b4-493c-a090-01125e0b8018})|({effa2f97-0f07-44c8-99cb-32ac760a0621})|({f6e6fd9b-b89f-4e8d-9257-01405bc139a6})|({ff87977a-fefb-4a9d-b703-4b73dce8853d})|({ffea9e62-e516-4238-88a7-d6f9346f4955}))$/">
<prefs/>
<versionRange minVersion="0" maxVersion="*" severity="3"/>
</emItem>
</emItems>
<pluginItems>
<pluginItem blockID="p332">

View file

@ -1,4 +1,6 @@
<!DOCTYPE html>
<html>
<head>
</head>
</html>

View file

@ -48,3 +48,6 @@ support-files =
file_favicon_redirect.html
file_favicon_redirect.ico
file_favicon_redirect.ico^headers^
[browser_rooticon.js]
support-files =
blank.html

View file

@ -0,0 +1,21 @@
add_task(async () => {
const testPath = "http://example.com/browser/browser/base/content/test/favicons/blank.html";
const expectedIcon = "http://example.com/favicon.ico";
let tab = BrowserTestUtils.addTab(gBrowser, testPath);
gBrowser.selectedTab = tab;
let browser = tab.linkedBrowser;
let faviconPromise = waitForLinkAvailable(browser);
await BrowserTestUtils.browserLoaded(browser);
let iconURI = await faviconPromise;
is(iconURI, expectedIcon, "Got correct initial icon.");
faviconPromise = waitForLinkAvailable(browser);
BrowserTestUtils.loadURI(browser, testPath);
await BrowserTestUtils.browserLoaded(browser);
iconURI = await faviconPromise;
is(iconURI, expectedIcon, "Got correct icon on second load.");
BrowserTestUtils.removeTab(tab);
});

View file

@ -5,6 +5,7 @@
// browsing mode don't persist once the private browsing window closes.
const CB_PREF = "browser.contentblocking.enabled";
const CB_UI_PREF = "browser.contentblocking.ui.enabled";
const TP_PB_PREF = "privacy.trackingprotection.enabled";
const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
var TrackingProtection = null;
@ -60,11 +61,13 @@ function testTrackingPage(window) {
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is set to blocked");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is set to blocked");
}
}
function testTrackingPageUnblocked() {
@ -85,11 +88,13 @@ function testTrackingPageUnblocked() {
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is not set to blocked");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is not set to blocked");
}
}
add_task(async function testExceptionAddition() {

View file

@ -15,6 +15,7 @@
*/
const CB_PREF = "browser.contentblocking.enabled";
const CB_UI_PREF = "browser.contentblocking.ui.enabled";
const TP_PREF = "privacy.trackingprotection.enabled";
const TP_PB_PREF = "privacy.trackingprotection.enabled";
const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
@ -66,7 +67,9 @@ function testBenignPage() {
ok(!hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is visible");
ok(hidden("#identity-popup-content-blocking-detected"), "blocking detected label is hidden");
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
}
}
function testBenignPageWithException() {
@ -90,7 +93,9 @@ function testBenignPageWithException() {
ok(!hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is visible");
ok(hidden("#identity-popup-content-blocking-detected"), "blocking detected label is hidden");
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(hidden("#identity-popup-content-blocking-category-list"), "category list is hidden");
}
}
function testTrackingPage(window) {
@ -117,11 +122,13 @@ function testTrackingPage(window) {
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is set to blocked");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is set to blocked");
}
}
function testTrackingPageUnblocked() {
@ -142,11 +149,13 @@ function testTrackingPageUnblocked() {
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is not set to blocked");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is not showing add blocking");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is not set to blocked");
}
}
function testTrackingPageWithCBDisabled() {
@ -166,11 +175,13 @@ function testTrackingPageWithCBDisabled() {
ok(hidden("#identity-popup-content-blocking-not-detected"), "blocking not detected label is hidden");
ok(!hidden("#identity-popup-content-blocking-detected"), "blocking detected label is visible");
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is showing add blocking");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is not set to blocked");
if (Services.prefs.getBoolPref(CB_UI_PREF)) {
ok(!hidden("#identity-popup-content-blocking-category-list"), "category list is visible");
ok(!hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-add-blocking"),
"TP category item is showing add blocking");
ok(hidden("#identity-popup-content-blocking-category-tracking-protection > .identity-popup-content-blocking-category-state-label"),
"TP category item is not set to blocked");
}
}
async function testContentBlockingEnabled(tab) {

View file

@ -220,7 +220,7 @@
enabled="false"
oncommand="ContentBlocking.onGlobalToggleCommand();" />
</toolbaritem>
<toolbarseparator id="appMenu-tp-separator" hidden="true" />
<toolbarseparator id="appMenu-tp-separator"/>
<toolbarbutton id="appMenu-new-window-button"
class="subviewbutton subviewbutton-iconic"
label="&newNavigatorCmd.label;"

View file

@ -2,6 +2,10 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(async function testSetup() {
Services.prefs.setBoolPref("toolkit.cosmeticAnimations.enabled", false);
});
add_task(async function testPopupSelectPopup() {
let extension = ExtensionTestUtils.loadExtension({
background() {
@ -46,6 +50,13 @@ add_task(async function testPopupSelectPopup() {
async function testPanel(browser) {
let popupPromise = promisePopupShown(selectPopup);
// Wait the select element in the popup window to be ready before sending a
// mouse event to open the select popup.
await ContentTask.spawn(browser, null, async () => {
await ContentTaskUtils.waitForCondition(() => {
return content.document && content.document.querySelector("#select");
});
});
BrowserTestUtils.synthesizeMouseAtCenter("#select", {}, browser);
await popupPromise;
@ -65,6 +76,11 @@ add_task(async function testPopupSelectPopup() {
is(Math.floor(boxObject.screenY + elemRect.bottom), popupRect.top,
"Select popup has the correct y origin");
// Close the select popup before proceeding to the next test.
const onPopupHidden = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
selectPopup.hidePopup();
await onPopupHidden;
}
{
@ -87,3 +103,7 @@ add_task(async function testPopupSelectPopup() {
await extension.unload();
});
add_task(async function testTeardown() {
Services.prefs.clearUserPref("toolkit.cosmeticAnimations.enabled");
});

View file

@ -69,7 +69,6 @@ payment-dialog > header > .page-error {
grid-area: main;
position: relative;
max-height: 100%;
padding-top: 18px;
}
.page {
@ -86,6 +85,7 @@ payment-dialog > header > .page-error {
flex-grow: 1;
/* The area above the footer should scroll, if necessary. */
overflow: auto;
padding-top: 18px;
}
.page > .page-body > h2:empty {

View file

@ -337,8 +337,10 @@ var PermissionPromptPrototype = {
this.permissionKey,
promptAction.action,
scope);
} else if (promptAction.action == SitePermissions.BLOCK) {
// Temporarily store BLOCK permissions only.
} else if (promptAction.action == SitePermissions.BLOCK ||
SitePermissions.permitTemporaryAllow(this.permissionKey)) {
// Temporarily store BLOCK permissions only unless permission object
// sets permitTemporaryAllow: true
// SitePermissions does not consider subframes when storing temporary
// permissions on a tab, thus storing ALLOW could be exploited.
SitePermissions.set(this.principal.URI,

View file

@ -11,7 +11,7 @@ var gStringBundle =
Services.strings.createBundle("chrome://browser/locale/sitePermissions.properties");
/**
* A helper module to manage temporarily blocked permissions.
* A helper module to manage temporary permissions.
*
* Permissions are keyed by browser, so methods take a Browser
* element to identify the corresponding permission set.
@ -20,7 +20,7 @@ var gStringBundle =
* automatically cleared once the browser stops existing
* (once there are no other references to the browser object);
*/
const TemporaryBlockedPermissions = {
const TemporaryPermissions = {
// This is a three level deep map with the following structure:
//
// Browser => {
@ -38,20 +38,20 @@ const TemporaryBlockedPermissions = {
// Private helper method that bundles some shared behavior for
// get() and getAll(), e.g. deleting permissions when they have expired.
_get(entry, prePath, id, timeStamp) {
if (timeStamp == null) {
_get(entry, prePath, id, permission) {
if (permission == null || permission.timeStamp == null) {
delete entry[prePath][id];
return null;
}
if (timeStamp + SitePermissions.temporaryPermissionExpireTime < Date.now()) {
if (permission.timeStamp + SitePermissions.temporaryPermissionExpireTime < Date.now()) {
delete entry[prePath][id];
return null;
}
return {id, state: SitePermissions.BLOCK, scope: SitePermissions.SCOPE_TEMPORARY};
return {id, state: permission.state, scope: SitePermissions.SCOPE_TEMPORARY};
},
// Sets a new permission for the specified browser.
set(browser, id) {
set(browser, id, state) {
if (!browser) {
return;
}
@ -63,7 +63,7 @@ const TemporaryBlockedPermissions = {
if (!entry[prePath]) {
entry[prePath] = {};
}
entry[prePath][id] = Date.now();
entry[prePath][id] = {timeStamp: Date.now(), state};
},
// Removes a permission with the specified id for the specified browser.
@ -292,7 +292,7 @@ var SitePermissions = {
getAllForBrowser(browser) {
let permissions = {};
for (let permission of TemporaryBlockedPermissions.getAll(browser)) {
for (let permission of TemporaryPermissions.getAll(browser)) {
permission.scope = this.SCOPE_TEMPORARY;
permissions[permission.id] = permission;
}
@ -417,6 +417,23 @@ var SitePermissions = {
return false;
},
/*
* Return whether SitePermissions is permitted to store a TEMPORARY ALLOW
* state for a particular permission.
*
* @param {string} permissionID
* The ID to get the state for.
*
* @return boolean Whether storing TEMPORARY ALLOW is permitted.
*/
permitTemporaryAllow(permissionID) {
if (permissionID in gPermissionObject &&
gPermissionObject[permissionID].permitTemporaryAllow)
return gPermissionObject[permissionID].permitTemporaryAllow;
return false;
},
/**
* Returns the state and scope of a particular permission for a given URI.
*
@ -461,7 +478,7 @@ var SitePermissions = {
if (result.state == defaultState) {
// If there's no persistent permission saved, check if we have something
// set temporarily.
let value = TemporaryBlockedPermissions.get(browser, permissionID);
let value = TemporaryPermissions.get(browser, permissionID);
if (value) {
result.state = value.state;
@ -520,9 +537,9 @@ var SitePermissions = {
// URI and only consider the current browser URI, to avoid notification spamming.
//
// If you ever consider removing this line, you likely want to implement
// a more fine-grained TemporaryBlockedPermissions that temporarily blocks for the
// a more fine-grained TemporaryPermissions that temporarily blocks for the
// entire browser, but temporarily allows only for specific frames.
if (state != this.BLOCK) {
if (state != this.BLOCK && !this.permitTemporaryAllow(permissionID)) {
throw "'Block' is the only permission we can save temporarily on a browser";
}
@ -530,7 +547,7 @@ var SitePermissions = {
throw "TEMPORARY scoped permissions require a browser object";
}
TemporaryBlockedPermissions.set(browser, permissionID);
TemporaryPermissions.set(browser, permissionID, state);
browser.dispatchEvent(new browser.ownerGlobal
.CustomEvent("PermissionStateChange"));
@ -562,10 +579,10 @@ var SitePermissions = {
if (this.isSupportedURI(uri))
Services.perms.remove(uri, permissionID);
// TemporaryBlockedPermissions.get() deletes expired permissions automatically,
if (TemporaryBlockedPermissions.get(browser, permissionID)) {
// TemporaryPermissions.get() deletes expired permissions automatically,
if (TemporaryPermissions.get(browser, permissionID)) {
// If it exists but has not expired, remove it explicitly.
TemporaryBlockedPermissions.remove(browser, permissionID);
TemporaryPermissions.remove(browser, permissionID);
// Send a PermissionStateChange event only if the permission hasn't expired.
browser.dispatchEvent(new browser.ownerGlobal
.CustomEvent("PermissionStateChange"));
@ -579,7 +596,7 @@ var SitePermissions = {
* The browser object to clear.
*/
clearTemporaryPermissions(browser) {
TemporaryBlockedPermissions.clear(browser);
TemporaryPermissions.clear(browser);
},
/**
@ -592,7 +609,7 @@ var SitePermissions = {
* The browser object to copy to.
*/
copyTemporaryPermissions(browser, newBrowser) {
TemporaryBlockedPermissions.copy(browser, newBrowser);
TemporaryPermissions.copy(browser, newBrowser);
},
/**
@ -702,6 +719,7 @@ var gPermissionObject = {
"autoplay-media": {
exactHostMatch: true,
showGloballyBlocked: true,
permitTemporaryAllow: true,
getDefault() {
let state = Services.prefs.getIntPref("media.autoplay.default",
Ci.nsIAutoplay.PROMPT);

View file

@ -19,6 +19,32 @@ add_task(async function testTempAllowThrows() {
});
});
// Tests that we can set TEMPORARY ALLOW permissions for autoplay-media
add_task(async function testTempAutoplayAllowed() {
Services.prefs.setIntPref("media.autoplay.default", 2);
let uri = Services.io.newURI("https://example.com");
let permId = "autoplay-media";
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri.spec);
SitePermissions.set(uri, permId, SitePermissions.ALLOW,
SitePermissions.SCOPE_TEMPORARY, tab.linkedBrowser);
let permissions = SitePermissions.getAllPermissionDetailsForBrowser(tab.linkedBrowser);
let autoplay = permissions.find(({id}) => id === "autoplay-media");
Assert.deepEqual(autoplay, {
id: "autoplay-media",
label: "Automatically Play Media with Sound",
state: SitePermissions.ALLOW,
scope: SitePermissions.SCOPE_TEMPORARY,
});
Services.prefs.clearUserPref("media.autoplay.default");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// This tests the SitePermissions.getAllPermissionDetailsForBrowser function.
add_task(async function testGetAllPermissionDetailsForBrowser() {
let uri = Services.io.newURI("https://example.com");

View file

@ -272,10 +272,11 @@
}
}
/* CONNECTION ICON, EXTENSION ICON */
/* CONNECTION ICON, EXTENSION ICON, REMOTE CONTROL ICON */
#connection-icon,
#extension-icon {
#extension-icon,
#remote-control-icon {
visibility: collapse;
}
#urlbar[pageproxystate="valid"] > #identity-box.verifiedDomain > #connection-icon,
@ -308,8 +309,6 @@
visibility: visible;
}
/* REMOTE CONTROL ICON */
#main-window[remotecontrol] #remote-control-icon {
list-style-image: url(chrome://browser/content/static-robot.png);
visibility: visible;

View file

@ -234,7 +234,7 @@ def bootstrap(topsrcdir, mozilla_dir=None):
def should_skip_dispatch(context, handler):
# The user is performing a maintenance command.
if handler.name in ('bootstrap', 'doctor', 'mach-commands', 'mercurial-setup'):
if handler.name in ('bootstrap', 'doctor', 'mach-commands', 'vcs-setup'):
return True
# We are running in automation.

View file

@ -1,7 +0,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/.
BROWSER_CHROME_MANIFESTS += [
'test/browser.ini',
]

View file

@ -1,10 +0,0 @@
"use strict";
module.exports = {
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../.eslintrc.mochitests.js",
"globals": {
"helpers": true,
"assert": true
}
};

View file

@ -1,13 +0,0 @@
[DEFAULT]
tags = devtools
subsuite = devtools
support-files =
head.js
helpers.js
mockCommands.js
# Bug 1447494 - browser.ini file has to have at least one test file
# browser.js is empty, we have this just to expose test helper files
# for other folder still using it
[browser.js]
skip-if = true

View file

@ -1,41 +0,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/. */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
/* import-globals-from helpers.js */
/* import-globals-from mockCommands.js */
"use strict";
const TEST_BASE_HTTP = "http://example.com/browser/devtools/client/commandline/test/";
const TEST_BASE_HTTPS = "https://example.com/browser/devtools/client/commandline/test/";
var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
var flags = require("devtools/shared/flags");
var { Task } = require("devtools/shared/task");
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js", this);
// Import the GCLI test helper
var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
Services.scriptloader.loadSubScript(testDir + "/mockCommands.js", this, "UTF-8");
function whenDelayedStartupFinished(aWindow, aCallback) {
Services.obs.addObserver(function observer(aSubject, aTopic) {
if (aWindow == aSubject) {
Services.obs.removeObserver(observer, aTopic);
executeSoon(aCallback);
}
}, "browser-delayed-startup-finished");
}
/**
* Force GC on shutdown, because it seems that GCLI can outrun the garbage
* collector in some situations, which causes test failures in later tests
* Bug 774619 is an example.
*/
registerCleanupFunction(function tearDown() {
window.windowUtils.garbageCollect();
});

File diff suppressed because it is too large Load diff

View file

@ -1,794 +0,0 @@
/*
* Copyright 2012, Mozilla Foundation and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
var mockCommands;
if (typeof exports !== "undefined") {
// If we're being loaded via require();
mockCommands = exports;
}
else {
// If we're being loaded via loadScript in mochitest
mockCommands = {};
}
// We use an alias for exports here because this module is used in Firefox
// mochitests where we don't have define/require
/**
* Registration and de-registration.
*/
mockCommands.setup = function (requisition) {
requisition.system.addItems(mockCommands.items);
};
mockCommands.shutdown = function (requisition) {
requisition.system.removeItems(mockCommands.items);
};
function createExec(name) {
return function (args, context) {
var promises = [];
Object.keys(args).map(argName => {
var value = args[argName];
var type = this.getParameterByName(argName).type;
var promise = Promise.resolve(type.stringify(value, context));
promises.push(promise.then(str => {
return { name: argName, value: str };
}));
});
return Promise.all(promises).then(data => {
var argValues = {};
data.forEach(function (entry) { argValues[entry.name] = entry.value; });
return context.typedData("testCommandOutput", {
name: name,
args: argValues
});
});
};
}
mockCommands.items = [
{
item: "converter",
from: "testCommandOutput",
to: "dom",
exec: function (testCommandOutput, context) {
var view = context.createView({
data: testCommandOutput,
html: "" +
"<table>" +
"<thead>" +
"<tr>" +
'<th colspan="3">Exec: ${name}</th>' +
"</tr>" +
"</thead>" +
"<tbody>" +
'<tr foreach="key in ${args}">' +
"<td> ${key}</td>" +
"<td>=</td>" +
"<td>${args[key]}</td>" +
"</tr>" +
"</tbody>" +
"</table>",
options: {
allowEval: true
}
});
return view.toDom(context.document);
}
},
{
item: "converter",
from: "testCommandOutput",
to: "string",
exec: function (testCommandOutput, context) {
var argsOut = Object.keys(testCommandOutput.args).map(function (key) {
return key + "=" + testCommandOutput.args[key];
}).join(" ");
return "Exec: " + testCommandOutput.name + " " + argsOut;
}
},
{
item: "type",
name: "optionType",
parent: "selection",
lookup: [
{
name: "option1",
value: "string"
},
{
name: "option2",
value: "number"
},
{
name: "option3",
value: {
name: "selection",
lookup: [
{ name: "one", value: 1 },
{ name: "two", value: 2 },
{ name: "three", value: 3 }
]
}
}
]
},
{
item: "type",
name: "optionValue",
parent: "delegate",
delegateType: function (executionContext) {
if (executionContext != null) {
var option = executionContext.getArgsObject().optionType;
if (option != null) {
return option;
}
}
return "blank";
}
},
{
item: "command",
name: "tsv",
params: [
{ name: "optionType", type: "optionType" },
{ name: "optionValue", type: "optionValue" }
],
exec: createExec("tsv")
},
{
item: "command",
name: "tsr",
params: [ { name: "text", type: "string" } ],
exec: createExec("tsr")
},
{
item: "command",
name: "tsrsrsr",
params: [
{ name: "p1", type: "string" },
{ name: "p2", type: "string" },
{ name: "p3", type: { name: "string", allowBlank: true} },
],
exec: createExec("tsrsrsr")
},
{
item: "command",
name: "tso",
params: [ { name: "text", type: "string", defaultValue: null } ],
exec: createExec("tso")
},
{
item: "command",
name: "tse",
params: [
{ name: "node", type: "node" },
{
group: "options",
params: [
{ name: "nodes", type: { name: "nodelist" } },
{ name: "nodes2", type: { name: "nodelist", allowEmpty: true } }
]
}
],
exec: createExec("tse")
},
{
item: "command",
name: "tsj",
params: [ { name: "javascript", type: "javascript" } ],
exec: createExec("tsj")
},
{
item: "command",
name: "tsb",
params: [ { name: "toggle", type: "boolean" } ],
exec: createExec("tsb")
},
{
item: "command",
name: "tss",
exec: createExec("tss")
},
{
item: "command",
name: "tsu",
params: [
{
name: "num",
type: {
name: "number",
max: 10,
min: -5,
step: 3
}
}
],
exec: createExec("tsu")
},
{
item: "command",
name: "tsf",
params: [
{
name: "num",
type: {
name: "number",
allowFloat: true,
max: 11.5,
min: -6.5,
step: 1.5
}
}
],
exec: createExec("tsf")
},
{
item: "command",
name: "tsn"
},
{
item: "command",
name: "tsn dif",
params: [ { name: "text", type: "string", description: "tsn dif text" } ],
exec: createExec("tsnDif")
},
{
item: "command",
name: "tsn hidden",
hidden: true,
exec: createExec("tsnHidden")
},
{
item: "command",
name: "tsn ext",
params: [ { name: "text", type: "string" } ],
exec: createExec("tsnExt")
},
{
item: "command",
name: "tsn exte",
params: [ { name: "text", type: "string" } ],
exec: createExec("tsnExte")
},
{
item: "command",
name: "tsn exten",
params: [ { name: "text", type: "string" } ],
exec: createExec("tsnExten")
},
{
item: "command",
name: "tsn extend",
params: [ { name: "text", type: "string" } ],
exec: createExec("tsnExtend")
},
{
item: "command",
name: "tsn deep"
},
{
item: "command",
name: "tsn deep down"
},
{
item: "command",
name: "tsn deep down nested"
},
{
item: "command",
name: "tsn deep down nested cmd",
exec: createExec("tsnDeepDownNestedCmd")
},
{
item: "command",
name: "tshidden",
hidden: true,
params: [
{
group: "Options",
params: [
{
name: "visible",
type: "string",
short: "v",
defaultValue: null,
description: "visible"
},
{
name: "invisiblestring",
type: "string",
short: "i",
description: "invisiblestring",
defaultValue: null,
hidden: true
},
{
name: "invisibleboolean",
short: "b",
type: "boolean",
description: "invisibleboolean",
hidden: true
}
]
}
],
exec: createExec("tshidden")
},
{
item: "command",
name: "tselarr",
params: [
{ name: "num", type: { name: "selection", data: [ "1", "2", "3" ] } },
{ name: "arr", type: { name: "array", subtype: "string" } }
],
exec: createExec("tselarr")
},
{
item: "command",
name: "tsm",
description: "a 3-param test selection|string|number",
params: [
{ name: "abc", type: { name: "selection", data: [ "a", "b", "c" ] } },
{ name: "txt", type: "string" },
{ name: "num", type: { name: "number", max: 42, min: 0 } }
],
exec: createExec("tsm")
},
{
item: "command",
name: "tsg",
description: "a param group test",
params: [
{
name: "solo",
type: { name: "selection", data: [ "aaa", "bbb", "ccc" ] },
description: "solo param"
},
{
group: "First",
params: [
{
name: "txt1",
type: "string",
defaultValue: null,
description: "txt1 param"
},
{
name: "bool",
type: "boolean",
description: "bool param"
}
]
},
{
name: "txt2",
type: "string",
defaultValue: "d",
description: "txt2 param",
option: "Second"
},
{
name: "num",
type: { name: "number", min: 40 },
defaultValue: 42,
description: "num param",
option: "Second"
}
],
exec: createExec("tsg")
},
{
item: "command",
name: "tscook",
description: "param group test to catch problems with cookie command",
params: [
{
name: "key",
type: "string",
description: "tscookKeyDesc"
},
{
name: "value",
type: "string",
description: "tscookValueDesc"
},
{
group: "tscookOptionsDesc",
params: [
{
name: "path",
type: "string",
defaultValue: "/",
description: "tscookPathDesc"
},
{
name: "domain",
type: "string",
defaultValue: null,
description: "tscookDomainDesc"
},
{
name: "secure",
type: "boolean",
description: "tscookSecureDesc"
}
]
}
],
exec: createExec("tscook")
},
{
item: "command",
name: "tslong",
description: "long param tests to catch problems with the jsb command",
params: [
{
name: "msg",
type: "string",
description: "msg Desc"
},
{
group: "Options Desc",
params: [
{
name: "num",
short: "n",
type: "number",
description: "num Desc",
defaultValue: 2
},
{
name: "sel",
short: "s",
type: {
name: "selection",
lookup: [
{ name: "space", value: " " },
{ name: "tab", value: "\t" }
]
},
description: "sel Desc",
defaultValue: " "
},
{
name: "bool",
short: "b",
type: "boolean",
description: "bool Desc"
},
{
name: "num2",
short: "m",
type: "number",
description: "num2 Desc",
defaultValue: -1
},
{
name: "bool2",
short: "c",
type: "boolean",
description: "bool2 Desc"
},
{
name: "sel2",
short: "t",
type: {
name: "selection",
data: [ "collapse", "basic", "with space", "with two spaces" ]
},
description: "sel2 Desc",
defaultValue: "collapse"
}
]
}
],
exec: createExec("tslong")
},
{
item: "command",
name: "tsdate",
description: "long param tests to catch problems with the jsb command",
params: [
{
name: "d1",
type: "date",
},
{
name: "d2",
type: {
name: "date",
min: "1 jan 2000",
max: "28 feb 2000",
step: 2
}
},
],
exec: createExec("tsdate")
},
{
item: "command",
name: "tsfail",
description: "test errors",
params: [
{
name: "method",
type: {
name: "selection",
data: [
"reject", "rejecttyped",
"throwerror", "throwstring", "throwinpromise",
"noerror"
]
}
}
],
exec: function (args, context) {
if (args.method === "reject") {
return new Promise(function (resolve, reject) {
context.environment.window.setTimeout(function () {
reject("rejected promise");
}, 10);
});
}
if (args.method === "rejecttyped") {
return new Promise(function (resolve, reject) {
context.environment.window.setTimeout(function () {
reject(context.typedData("number", 54));
}, 10);
});
}
if (args.method === "throwinpromise") {
return new Promise(function (resolve, reject) {
context.environment.window.setTimeout(function () {
resolve("should be lost");
}, 10);
}).then(function () {
var t = null;
return t.foo;
});
}
if (args.method === "throwerror") {
throw new Error("thrown error");
}
if (args.method === "throwstring") {
throw "thrown string";
}
return "no error";
}
},
{
item: "command",
name: "tsfile",
description: "test file params",
},
{
item: "command",
name: "tsfile open",
description: "a file param in open mode",
params: [
{
name: "p1",
type: {
name: "file",
filetype: "file",
existing: "yes"
}
}
],
exec: createExec("tsfile open")
},
{
item: "command",
name: "tsfile saveas",
description: "a file param in saveas mode",
params: [
{
name: "p1",
type: {
name: "file",
filetype: "file",
existing: "no"
}
}
],
exec: createExec("tsfile saveas")
},
{
item: "command",
name: "tsfile save",
description: "a file param in save mode",
params: [
{
name: "p1",
type: {
name: "file",
filetype: "file",
existing: "maybe"
}
}
],
exec: createExec("tsfile save")
},
{
item: "command",
name: "tsfile cd",
description: "a file param in cd mode",
params: [
{
name: "p1",
type: {
name: "file",
filetype: "directory",
existing: "yes"
}
}
],
exec: createExec("tsfile cd")
},
{
item: "command",
name: "tsfile mkdir",
description: "a file param in mkdir mode",
params: [
{
name: "p1",
type: {
name: "file",
filetype: "directory",
existing: "no"
}
}
],
exec: createExec("tsfile mkdir")
},
{
item: "command",
name: "tsfile rm",
description: "a file param in rm mode",
params: [
{
name: "p1",
type: {
name: "file",
filetype: "any",
existing: "yes"
}
}
],
exec: createExec("tsfile rm")
},
{
item: "command",
name: "tsslow",
params: [
{
name: "hello",
type: {
name: "selection",
data: function (context) {
return new Promise(function (resolve, reject) {
context.environment.window.setTimeout(function () {
resolve([
"Shalom", "Namasté", "Hallo", "Dydd-da",
"Chào", "Hej", "Saluton", "Sawubona"
]);
}, 10);
});
}
}
}
],
exec: function (args, context) {
return "Test completed";
}
},
{
item: "command",
name: "urlc",
params: [
{
name: "url",
type: "url"
}
],
returnType: "json",
exec: function (args, context) {
return args;
}
},
{
item: "command",
name: "unionc1",
params: [
{
name: "first",
type: {
name: "union",
alternatives: [
{
name: "selection",
lookup: [
{ name: "one", value: 1 },
{ name: "two", value: 2 },
]
},
"number",
{ name: "string" }
]
}
}
],
returnType: "json",
exec: function (args, context) {
return args;
}
},
{
item: "command",
name: "unionc2",
params: [
{
name: "first",
type: {
name: "union",
alternatives: [
{
name: "selection",
lookup: [
{ name: "one", value: 1 },
{ name: "two", value: 2 },
]
},
{
name: "url"
}
]
}
}
],
returnType: "json",
exec: function (args, context) {
return args;
}
},
{
item: "command",
name: "tsres",
params: [
{
name: "resource",
type: "resource"
}
],
exec: createExec("tsres"),
}
];

View file

@ -5,7 +5,6 @@ skip-if = (os == 'linux' && debug && bits == 32)
support-files =
head.js
helpers.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
## START-SOURCEMAPPED-FIXTURES - Generated by examples/sourcemapped/build.js

View file

@ -128,7 +128,6 @@ support-files =
sjs_post-page.sjs
sjs_random-javascript.sjs
testactors.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js

View file

@ -128,7 +128,6 @@ support-files =
sjs_post-page.sjs
sjs_random-javascript.sjs
testactors.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js

View file

@ -66,8 +66,6 @@ registerCleanupFunction(async function() {
var testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
testDir = testDir.replace(/\/\//g, "/");
testDir = testDir.replace("chrome:/mochitest", "chrome://mochitest");
var helpersjs = testDir + "/../../../commandline/test/helpers.js";
Services.scriptloader.loadSubScript(helpersjs, this);
function addWindow(aUrl) {
info("Adding window: " + aUrl);
@ -669,7 +667,7 @@ AddonDebugger.prototype = {
this.client.addListener("consoleAPICall", this._onConsoleAPICall);
deferred.resolve();
}, e => {
deferred.reject(e);
deferred.reject(e);
});
return deferred.promise;
},

View file

@ -6,5 +6,76 @@
</head>
<body>
<h1>SW-test</h1>
<script>
function register() {
return Promise.resolve().then(function() {
// While ServiceWorkerContainer.register() returns a promise, it's
// still wrapped with a .then() because navigator.serviceWorker is not
// defined in insecure contexts unless service worker testing is
// enabled, so dereferencing it would throw a ReferenceError (which
// is then caught in the .catch() clause).
return window.navigator.serviceWorker.register("serviceworker.js");
}).then(registration => {
return {success: true};
}).catch(error => {
return {success: false};
});
}
function unregister() {
return Promise.resolve().then(function() {
return window.navigator.serviceWorker.getRegistration();
}).then(registration => {
return registration.unregister().then(result => {
return {success: !!result};
});
}).catch(_ => {
return {success: false};
});
}
function iframeRegisterAndUnregister() {
var frame = window.document.createElement("iframe");
var promise = new Promise(function(resolve, reject) {
frame.addEventListener("load", function() {
Promise.resolve().then(_ => {
return frame.contentWindow.navigator.serviceWorker.register("serviceworker.js");
}).then(swr => {
return swr.unregister();
}).then(_ => {
frame.remove();
resolve({success: true});
}).catch(error => {
resolve({success: false});
});
}, {once: true});
});
frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
window.document.body.appendChild(frame);
return promise;
}
window.addEventListener("message", function(event) {
var response;
switch (event.data) {
case "devtools:sw-test:register": {
response = register();
break;
}
case "devtools:sw-test:unregister": {
response = unregister();
break;
}
case "devtools:sw-test:iframe:register-and-unregister": {
response = iframeRegisterAndUnregister();
break;
}
}
response.then(data => {
event.ports[0].postMessage(data);
event.ports[0].close();
});
});
</script>
</body>
</html>

View file

@ -9,39 +9,23 @@
"use strict";
addMessageListener("devtools:sw-test:register", function(msg) {
content.navigator.serviceWorker.register("serviceworker.js")
.then(swr => {
sendAsyncMessage("devtools:sw-test:register", {success: true});
}, error => {
sendAsyncMessage("devtools:sw-test:register", {success: false});
});
});
addMessageListener("devtools:sw-test:unregister", function(msg) {
content.navigator.serviceWorker.getRegistration().then(swr => {
swr.unregister().then(result => {
sendAsyncMessage("devtools:sw-test:unregister",
{success: !!result});
});
function addMessageForwarder(name) {
addMessageListener(name, function(_) {
var channel = new MessageChannel();
content.postMessage(name, "*", [channel.port2]);
channel.port1.onmessage = function(msg) {
sendAsyncMessage(name, msg.data);
channel.port1.close();
};
});
});
}
addMessageListener("devtools:sw-test:iframe:register-and-unregister", function(msg) {
var frame = content.document.createElement("iframe");
frame.addEventListener("load", function() {
frame.contentWindow.navigator.serviceWorker.register("serviceworker.js")
.then(swr => {
return swr.unregister();
}).then(_ => {
frame.remove();
sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
{success: true});
}).catch(error => {
sendAsyncMessage("devtools:sw-test:iframe:register-and-unregister",
{success: false});
});
}, {once: true});
frame.src = "browser_toolbox_options_enabled_serviceworkers_testing.html";
content.document.body.appendChild(frame);
});
const messages = [
"devtools:sw-test:register",
"devtools:sw-test:unregister",
"devtools:sw-test:iframe:register-and-unregister"
];
for (var msg of messages) {
addMessageForwarder(msg);
}

View file

@ -19,7 +19,6 @@ support-files =
doc_multiple_property_types.html
doc_timing_combination_animation.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/frame-script-utils.js

View file

@ -5,7 +5,6 @@ support-files =
doc_boxmodel_iframe1.html
doc_boxmodel_iframe2.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -10,7 +10,6 @@ support-files =
doc_sourcemaps.html
doc_sourcemaps.scss
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -4,7 +4,6 @@ subsuite = devtools
support-files =
head.js
head_devtools_inspector_sidebar.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -8,7 +8,6 @@ support-files =
ostrich-black.ttf
ostrich-regular.ttf
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -4,7 +4,6 @@ subsuite = devtools
support-files =
doc_iframe_reloaded.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -69,7 +69,6 @@ support-files =
lib_react_with_addons_15.3.1_min.js
lib_react_with_addons_15.4.1.js
react_external_listeners.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/debugger/new/test/mochitest/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js

View file

@ -41,7 +41,6 @@ registerCleanupFunction(() => {
* @param {String} filePath The file path, relative to the current directory.
* Examples:
* - "helper_attributes_test_runner.js"
* - "../../../commandline/test/helpers.js"
*/
function loadHelperScript(filePath) {
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));

View file

@ -113,23 +113,9 @@ function parseAttributeValues(attr, doc) {
return attributes;
}
/**
* Truncate the string and add ellipsis to the middle of the string.
*/
function truncateString(str, maxLength) {
if (!str || str.length <= maxLength) {
return str;
}
return str.substring(0, Math.ceil(maxLength / 2)) +
"…" +
str.substring(str.length - Math.floor(maxLength / 2));
}
module.exports = {
flashElementOn,
flashElementOff,
getAutocompleteMaxWidth,
parseAttributeValues,
truncateString,
};

View file

@ -11,8 +11,8 @@ const {
flashElementOn,
flashElementOff,
parseAttributeValues,
truncateString,
} = require("devtools/client/inspector/markup/utils");
const { truncateString } = require("devtools/shared/inspector/utils");
const {editableField, InplaceEditor} =
require("devtools/client/shared/inplace-editor");
const {parseAttribute} =

View file

@ -40,7 +40,6 @@ support-files =
doc_variables_1.html
doc_variables_2.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -13,7 +13,6 @@ support-files =
doc_content_stylesheet_xul.css
doc_frame_script.js
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js

View file

@ -42,7 +42,6 @@ support-files =
head.js
img_browser_inspector_highlighter-eyedropper-image.png
shared-head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
!/devtools/client/shared/test/test-actor.js

View file

@ -18,7 +18,6 @@ support-files =
simple_json.json^headers^
valid_json.json
valid_json.json^headers^
!/devtools/client/commandline/test/head.js
!/devtools/client/framework/test/head.js
!/devtools/client/shared/test/frame-script-utils.js
!/devtools/client/shared/test/shared-head.js

View file

@ -12,7 +12,6 @@ DIRS += [
'accessibility',
'application',
'canvasdebugger',
'commandline',
'debugger',
'dom',
'framework',

View file

@ -13,7 +13,6 @@ support-files =
geolocation.html
head.js
touch.html
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/shared-redux-head.js

View file

@ -6,7 +6,6 @@
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../../shared/test/shared-head.js */
/* import-globals-from ../../../shared/test/shared-redux-head.js */
/* import-globals-from ../../../commandline/test/helpers.js */
/* import-globals-from ../../../inspector/test/shared-head.js */
Services.scriptloader.loadSubScript(
@ -16,11 +15,6 @@ Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
this);
// Import the GCLI test helper
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
this);
// Import helpers registering the test-actor in remote targets
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",

View file

@ -368,7 +368,6 @@ function once(target, eventName, useCapture = false) {
* @param {String} filePath The file path, relative to the current directory.
* Examples:
* - "helper_attributes_test_runner.js"
* - "../../../commandline/test/helpers.js"
*/
function loadHelperScript(filePath) {
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));

View file

@ -83,7 +83,6 @@ function teardown(ed, win) {
* @param {String} filePath The file path, relative to the current directory.
* Examples:
* - "helper_attributes_test_runner.js"
* - "../../../commandline/test/helpers.js"
*/
function loadHelperScript(filePath) {
const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));

View file

@ -60,7 +60,6 @@ support-files =
doc_xulpage.xul
sync.html
utf-16.css
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/shared/test/head.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js

View file

@ -348,6 +348,12 @@ const AccessibleActor = ActorClassWithSpec(accessibleSpec, {
return null;
}
// Check if accessible bounds are invalid.
const left = x, right = x + w, top = y, bottom = y + h;
if (left === right || top === bottom) {
return null;
}
return { x, y, w, h };
},
@ -386,9 +392,13 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
this.onPick = this.onPick.bind(this);
this.onHovered = this.onHovered.bind(this);
this.onKey = this.onKey.bind(this);
this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
this.highlighter = CustomHighlighterActor(this, isXUL(this.rootWin) ?
"XULWindowAccessibleHighlighter" : "AccessibleHighlighter");
this.manage(this.highlighter);
this.highlighter.on("highlighter-event", this.onHighlighterEvent);
},
setA11yServiceGetter() {
@ -441,7 +451,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
this.reset();
this.highlighter.destroy();
this.highlighter.off("highlighter-event", this.onHighlighterEvent);
this.highlighter = null;
this.targetActor = null;
@ -585,6 +595,10 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
{ accessible: parent, children: parent.children() }));
},
onHighlighterEvent: function(data) {
this.emit("highlighter-event", data);
},
/**
* Accessible event observer function.
*
@ -700,13 +714,13 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
* True if highlighter shows the accessible object.
*/
highlightAccessible(accessible, options = {}) {
const bounds = accessible.bounds;
const { bounds, name, role } = accessible;
if (!bounds) {
return false;
}
return this.highlighter.show({ rawNode: accessible.rawAccessible.DOMNode },
{ ...options, ...bounds });
{ ...options, ...bounds, name, role });
},
/**
@ -793,9 +807,13 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
}
if (this._currentAccessible !== accessible) {
const { bounds } = accessible;
const { bounds, role, name } = accessible;
if (bounds) {
this.highlighter.show({ rawNode: event.originalTarget || event.target }, bounds);
this.highlighter.show({ rawNode: event.originalTarget || event.target }, {
...bounds,
role,
name
});
}
events.emit(this, "picker-accessible-hovered", accessible);

View file

@ -35,6 +35,7 @@
--highlighter-bubble-arrow-size: 8px;
--highlighter-font-family: message-box;
--highlighter-font-size: 11px;
--highlighter-infobar-color: hsl(210, 30%, 85%);
--highlighter-marker-color: #000;
}
@ -210,7 +211,7 @@
}
:-moz-native-anonymous [class$=infobar-dimensions] {
color: hsl(210, 30%, 85%);
color: var(--highlighter-infobar-color);
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;
padding-inline-start: 6px;
@ -243,7 +244,7 @@
}
:-moz-native-anonymous .css-grid-line-infobar-names:not(:empty) {
color: hsl(210, 30%, 85%);
color: var(--highlighter-infobar-color);
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;
padding-inline-start: 6px;
@ -645,3 +646,19 @@
opacity: 0.6;
fill: #6a5acd;
}
:-moz-native-anonymous .accessible-infobar-name {
color:var(--highlighter-infobar-color);
max-width: 90%;
}
:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
color: var(--highlighter-infobar-color);
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;
padding-inline-start: 6px;
}
:-moz-native-anonymous .accessible-infobar-role {
color: #9CDCFE;
}

View file

@ -5,12 +5,11 @@
"use strict";
const { AutoRefreshHighlighter } = require("./auto-refresh");
const { getBounds } = require("./utils/accessibility");
const { getBounds, Infobar } = require("./utils/accessibility");
const {
CanvasFrameAnonymousContentHelper,
createNode,
createSVGNode
createSVGNode,
} = require("./utils/markup");
const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
@ -36,21 +35,33 @@ const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
* height of the the accessible object
* - {Number} duration
* Duration of time that the highlighter should be shown.
* - {String|null} name
* name of the the accessible object
* - {String} role
* role of the the accessible object
*
* Structure:
* <div class="highlighter-container">
* <div class="highlighter-container" aria-hidden="true">
* <div class="accessible-root">
* <svg class="accessible-elements" hidden="true">
* <path class="accessible-bounds" points="..." />
* </svg>
* <div class="accessible-infobar-container">
* <div class="accessible-infobar">
* <div class="accessible-infobar-text">
* <span class="accessible-infobar-role">Accessible Role</span>
* <span class="accessible-infobar-name">Accessible Name</span>
* </div>
* </div>
* </div>
* </div>
* </div>
*/
class AccessibleHighlighter extends AutoRefreshHighlighter {
constructor(highlighterEnv) {
super(highlighterEnv);
this.ID_CLASS_PREFIX = "accessible-";
this.accessibleInfobar = new Infobar(this);
this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
this._buildMarkup.bind(this));
@ -73,7 +84,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
const container = createNode(this.win, {
attributes: {
"class": "highlighter-container",
"role": "presentation"
"aria-hidden": "true"
}
});
@ -82,7 +93,6 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
attributes: {
"id": "root",
"class": "root",
"role": "presentation"
},
prefix: this.ID_CLASS_PREFIX
});
@ -96,7 +106,6 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
"width": "100%",
"height": "100%",
"hidden": "true",
"role": "presentation"
},
prefix: this.ID_CLASS_PREFIX
});
@ -107,11 +116,13 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
attributes: {
"class": "bounds",
"id": "bounds",
"role": "presentation"
},
prefix: this.ID_CLASS_PREFIX
});
// Build the accessible's infobar markup.
this.accessibleInfobar.buildMarkup(root);
return container;
}
@ -127,6 +138,8 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
this.highlighterEnv.off("will-navigate", this.onWillNavigate);
this.pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
this.pageListenerTarget = null;
this.accessibleInfobar.destroy();
this.accessibleInfobar = null;
this.markup.destroy();
AutoRefreshHighlighter.prototype.destroy.call(this);
@ -156,11 +169,15 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
const { duration } = this.options;
const shown = this._update();
if (shown && duration) {
this._highlightTimer = setTimeout(() => {
this.hide();
}, duration);
if (shown) {
this.emit("highlighter-event", { options: this.options, type: "shown"});
if (duration) {
this._highlightTimer = setTimeout(() => {
this.hide();
}, duration);
}
}
return shown;
}
@ -175,6 +192,9 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
if (this._updateAccessibleBounds()) {
this._showAccessibleBounds();
this.accessibleInfobar.show();
shown = true;
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
@ -193,6 +213,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
_hide() {
setIgnoreLayoutChanges(true);
this._hideAccessibleBounds();
this.accessibleInfobar.hide();
setIgnoreLayoutChanges(false,
this.highlighterEnv.window.document.documentElement);
}
@ -205,7 +226,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
}
/**
* Showthe accessible bounds container.
* Show the accessible bounds container.
*/
_showAccessibleBounds() {
this.getElement("elements").removeAttribute("hidden");
@ -230,7 +251,7 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
_updateAccessibleBounds() {
const bounds = this._bounds;
if (!bounds) {
this._hideAccessibleBounds();
this._hide();
return false;
}

View file

@ -4,7 +4,356 @@
"use strict";
const { getCurrentZoom } = require("devtools/shared/layout/utils");
const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layout/utils");
const { moveInfobar, createNode } = require("./markup");
const { truncateString } = require("devtools/shared/inspector/utils");
// Max string length for truncating accessible name values.
const MAX_STRING_LENGTH = 50;
/**
* The AccessibleInfobar is a class responsible for creating the markup for the
* accessible highlighter. It is also reponsible for updating content within the
* infobar such as role and name values.
*/
class Infobar {
constructor(highlighter) {
this.highlighter = highlighter;
}
get document() {
return this.highlighter.win.document;
}
get bounds() {
return this.highlighter._bounds;
}
get options() {
return this.highlighter.options;
}
get prefix() {
return this.highlighter.ID_CLASS_PREFIX;
}
get win() {
return this.highlighter.win;
}
/**
* Move the Infobar to the right place in the highlighter.
*
* @param {Element} container
* Container of infobar.
*/
_moveInfobar(container) {
// Position the infobar using accessible's bounds
const { left: x, top: y, bottom, width } = this.bounds;
const infobarBounds = { x, y, bottom, width };
moveInfobar(container, infobarBounds, this.win);
}
/**
* Build markup for infobar.
*
* @param {Element} root
* Root element to build infobar with.
*/
buildMarkup(root) {
const container = createNode(this.win, {
parent: root,
attributes: {
"class": "infobar-container",
"id": "infobar-container",
"aria-hidden": "true",
"hidden": "true"
},
prefix: this.prefix,
});
const infobar = createNode(this.win, {
parent: container,
attributes: {
"class": "infobar",
"id": "infobar",
},
prefix: this.prefix,
});
const infobarText = createNode(this.win, {
parent: infobar,
attributes: {
"class": "infobar-text",
"id": "infobar-text",
},
prefix: this.prefix,
});
createNode(this.win, {
nodeType: "span",
parent: infobarText,
attributes: {
"class": "infobar-role",
"id": "infobar-role",
},
prefix: this.prefix,
});
createNode(this.win, {
nodeType: "span",
parent: infobarText,
attributes: {
"class": "infobar-name",
"id": "infobar-name",
},
prefix: this.prefix,
});
}
/**
* Destroy the Infobar's highlighter.
*/
destroy() {
this.highlighter = null;
}
/**
* Gets the element with the specified ID.
*
* @param {String} id
* Element ID.
* @return {Element} The element with specified ID.
*/
getElement(id) {
return this.highlighter.getElement(id);
}
/**
* Gets the text content of element.
*
* @param {String} id
* Element ID to retrieve text content from.
* @return {String} The text content of the element.
*/
getTextContent(id) {
const anonymousContent = this.highlighter.markup.content;
return anonymousContent.getTextContentForElement(`${this.prefix}${id}`);
}
/**
* Hide the accessible infobar.
*/
hide() {
const container = this.getElement("infobar-container");
container.setAttribute("hidden", "true");
}
/**
* Show the accessible infobar highlighter.
*/
show() {
const container = this.getElement("infobar-container");
// Remove accessible's infobar "hidden" attribute. We do this first to get the
// computed styles of the infobar container.
container.removeAttribute("hidden");
// Update the infobar's position and content.
this.update(container);
}
/**
* Update content of the infobar.
*/
update(container) {
const { name, role } = this.options;
this.updateRole(role, this.getElement("infobar-role"));
this.updateName(name, this.getElement("infobar-name"));
// Position the infobar.
this._moveInfobar(container);
}
/**
* Sets the text content of the specified element.
*
* @param {Element} el
* Element to set text content on.
* @param {String} text
* Text for content.
*/
setTextContent(el, text) {
el.setTextContent(text);
}
/**
* Show the accessible's name message.
*
* @param {String} name
* Accessible's name value.
* @param {Element} el
* Element to set text content on.
*/
updateName(name, el) {
const nameText = name ? `"${truncateString(name, MAX_STRING_LENGTH)}"` : "";
this.setTextContent(el, nameText);
}
/**
* Show the accessible's role.
*
* @param {String} role
* Accessible's role value.
* @param {Element} el
* Element to set text content on.
*/
updateRole(role, el) {
this.setTextContent(el, role);
}
}
/**
* The XULAccessibleInfobar handles building the XUL infobar markup where it isn't
* possible with the regular accessible highlighter.
*/
class XULWindowInfobar extends Infobar {
/**
* A helper function that calculates the positioning of a XUL accessible's infobar.
*
* @param {Object} container
* The infobar container.
*/
_moveInfobar(container) {
const arrow = this.getElement("arrow");
// Show the container and arrow elements first.
container.removeAttribute("hidden");
arrow.removeAttribute("hidden");
// Set the left value of the infobar container in relation to
// highlighter's bounds position.
const {
left: boundsLeft,
right: boundsRight,
top: boundsTop,
bottom: boundsBottom
} = this.bounds;
const boundsMidPoint = (boundsLeft + boundsRight) / 2;
container.style.left = `${boundsMidPoint}px`;
const zoom = getCurrentZoom(this.win);
let {
width: viewportWidth,
height: viewportHeight,
} = getViewportDimensions(this.win);
const { width, height, left, } = container.getBoundingClientRect();
const containerHalfWidth = width / 2;
const containerHeight = height;
const margin = 100 * zoom;
viewportHeight *= zoom;
viewportWidth *= zoom;
// Determine viewport boundaries for infobar.
const topBoundary = margin;
const bottomBoundary = viewportHeight - containerHeight;
const leftBoundary = containerHalfWidth;
const rightBoundary = viewportWidth - containerHalfWidth;
// Determine if an infobar's position is offscreen.
const isOffScreenOnTop = boundsBottom < topBoundary;
const isOffScreenOnBottom = boundsBottom > bottomBoundary;
const isOffScreenOnLeft = left < leftBoundary;
const isOffScreenOnRight = left > rightBoundary;
// Check if infobar is offscreen on either left/right of viewport and position.
if (isOffScreenOnLeft) {
container.style.left = `${leftBoundary + boundsLeft}px`;
arrow.setAttribute("hidden", "true");
} else if (isOffScreenOnRight) {
const leftOffset = rightBoundary - boundsRight;
container.style.left = `${rightBoundary - leftOffset - containerHalfWidth}px`;
arrow.setAttribute("hidden", "true");
}
// Check if infobar is offscreen on either top/bottom of viewport and position.
const bubbleArrowSize = "var(--highlighter-bubble-arrow-size)";
if (isOffScreenOnTop) {
if (boundsTop < 0) {
container.style.top = bubbleArrowSize;
} else {
container.style.top = `calc(${boundsBottom}px + ${bubbleArrowSize})`;
}
arrow.setAttribute("class", "accessible-arrow top");
} else if (isOffScreenOnBottom) {
container.style.top = `calc(${bottomBoundary}px - ${bubbleArrowSize})`;
arrow.setAttribute("hidden", "true");
} else {
container.style.top = `calc(${boundsTop}px -
(${containerHeight}px + ${bubbleArrowSize}))`;
arrow.setAttribute("class", "accessible-arrow bottom");
}
}
/**
* Build markup for XUL window infobar.
*
* @param {Element} root
* Root element to build infobar with.
*/
buildMarkup(root) {
super.buildMarkup(root, createNode);
createNode(this.win, {
parent: this.getElement("infobar"),
attributes: {
"class": "arrow",
"id": "arrow",
},
prefix: this.prefix,
});
}
/**
* Override of Infobar class's getTextContent method.
*
* @param {String} id
* Element ID to retrieve text content from.
* @return {String} Returns the text content of the element.
*/
getTextContent(id) {
return this.getElement(id).textContent;
}
/**
* Override of Infobar class's getElement method.
*
* @param {String} id
* Element ID.
* @return {String} Returns the specified element.
*/
getElement(id) {
return this.win.document.getElementById(`${this.prefix}${id}`);
}
/**
* Override of Infobar class's setTextContent method.
*
* @param {Element} el
* Element to set text content on.
* @param {String} text
* Text for content.
*/
setTextContent(el, text) {
el.textContent = text;
}
}
/**
* A helper function that calculate accessible object bounds and positioning to
@ -30,7 +379,10 @@ const { getCurrentZoom } = require("devtools/shared/layout/utils");
function getBounds(win, { x, y, w, h, zoom }) {
let { mozInnerScreenX, mozInnerScreenY, scrollX, scrollY } = win;
let zoomFactor = getCurrentZoom(win);
let left = x, right = x + w, top = y, bottom = y + h;
let left = x;
let right = x + w;
let top = y;
let bottom = y + h;
// For a XUL accessible, normalize the top-level window with its current zoom level.
// We need to do this because top-level browser content does not allow zooming.
@ -58,4 +410,7 @@ function getBounds(win, { x, y, w, h, zoom }) {
return { left, right, top, bottom, width, height };
}
exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
exports.getBounds = getBounds;
exports.Infobar = Infobar;
exports.XULWindowInfobar = XULWindowInfobar;

View file

@ -4,7 +4,7 @@
"use strict";
const { getBounds } = require("./utils/accessibility");
const { getBounds, XULWindowInfobar } = require("./utils/accessibility");
const { createNode, isNodeValid } = require("./utils/markup");
const { getCurrentZoom, loadSheet } = require("devtools/shared/layout/utils");
@ -13,6 +13,12 @@ const { getCurrentZoom, loadSheet } = require("devtools/shared/layout/utils");
* is consistent with the styling of an in-content accessible highlighter.
*/
const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
.highlighter-container {
--highlighter-bubble-background-color: hsl(214, 13%, 24%);
--highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
--highlighter-bubble-arrow-size: 8px;
}
.accessible-bounds {
position: fixed;
pointer-events: none;
@ -20,6 +26,66 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
display: block;
background-color: #6a5acd!important;
opacity: 0.6;
}
.accessible-infobar-container {
position: fixed;
max-width: 90%;
z-index: 11;
}
.accessible-infobar {
position: relative;
left: -50%;
background-color: var(--highlighter-bubble-background-color);
min-width: 75px;
border: 1px solid var(--highlighter-bubble-border-color);
border-radius: 3px;
padding: 5px;
}
.accessible-arrow {
position: absolute;
width: 0;
height: 0;
border-left: var(--highlighter-bubble-arrow-size) solid transparent;
border-right: var(--highlighter-bubble-arrow-size) solid transparent;
left: calc(50% - var(--highlighter-bubble-arrow-size));
}
.top {
border-bottom: var(--highlighter-bubble-arrow-size) solid
var(--highlighter-bubble-background-color);
top: calc(-1 * var(--highlighter-bubble-arrow-size));
}
.bottom {
border-top: var(--highlighter-bubble-arrow-size) solid
var(--highlighter-bubble-background-color);
bottom: calc(-1 * var(--highlighter-bubble-arrow-size));
}
.accessible-infobar-text {
overflow: hidden;
white-space: nowrap;
display: flex;
justify-content: center;
}
.accessible-infobar-name {
color: rgb(221, 0, 169);
max-width: 90%;
}
.accessible-infobar-name:not(:empty) {
color: hsl(210, 30%, 85%);
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;
padding-inline-start: 6px;
}
.accessible-infobar-role {
color: #9CDCFE;
}`);
/**
@ -37,8 +103,10 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
*/
class XULWindowAccessibleHighlighter {
constructor(highlighterEnv) {
this.ID_CLASS_PREFIX = "accessible-";
this.highlighterEnv = highlighterEnv;
this.win = highlighterEnv.window;
this.accessibleInfobar = new XULWindowInfobar(this);
}
/**
@ -60,7 +128,7 @@ class XULWindowAccessibleHighlighter {
parent: doc.body || doc.documentElement,
attributes: {
"class": "highlighter-container",
"role": "presentation"
"aria-hidden": "true"
}
});
@ -68,9 +136,10 @@ class XULWindowAccessibleHighlighter {
parent: this.container,
attributes: {
"class": "accessible-bounds",
"role": "presentation"
}
});
this.accessibleInfobar.buildMarkup(this.container);
}
/**
@ -104,6 +173,11 @@ class XULWindowAccessibleHighlighter {
* height of the the accessible object
* - duration {Number}
* Duration of time that the highlighter should be shown.
* - {String|null} name
* name of the the accessible object
* - {String} role
* role of the the accessible object
*
* @return {Boolean} True if accessible is highlighted, false otherwise.
*/
show(node, options = {}) {
@ -158,6 +232,7 @@ class XULWindowAccessibleHighlighter {
}
let boundsEl = this.bounds;
if (!boundsEl) {
this._buildMarkup();
boundsEl = this.bounds;
@ -168,7 +243,9 @@ class XULWindowAccessibleHighlighter {
boundsEl.style.left = `${left}px`;
boundsEl.style.width = `${width}px`;
boundsEl.style.height = `${height}px`;
this._showAccessibleBounds();
this.accessibleInfobar.show();
return true;
}
@ -182,6 +259,8 @@ class XULWindowAccessibleHighlighter {
}
this._hideAccessibleBounds();
this.accessibleInfobar.hide();
this.currentNode = null;
this.options = null;
}
@ -218,6 +297,9 @@ class XULWindowAccessibleHighlighter {
this.container.remove();
}
this.accessibleInfobar.destroy();
this.accessibleInfobar = null;
this.win = null;
}
}

View file

@ -4,6 +4,7 @@ subsuite = devtools
support-files =
head.js
animation.html
doc_accessibility_infobar.html
doc_accessibility.html
doc_allocations.html
doc_force_cc.html
@ -30,6 +31,8 @@ support-files =
!/devtools/client/shared/test/telemetry-test-helpers.js
!/devtools/server/tests/mochitest/hello-actor.js
[browser_accessibility_highlighter_infobar.js]
[browser_accessibility_infobar_show.js]
[browser_accessibility_node.js]
[browser_accessibility_node_events.js]
[browser_accessibility_simple.js]

View file

@ -0,0 +1,59 @@
/* 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";
// Test the accessible highlighter's infobar content.
const { truncateString } = require("devtools/shared/inspector/utils");
const { MAX_STRING_LENGTH } = require("devtools/server/actors/highlighters/utils/accessibility");
add_task(async function() {
const { client, walker, accessibility } =
await initAccessibilityFrontForUrl(MAIN_DOMAIN + "doc_accessibility_infobar.html");
const a11yWalker = await accessibility.getWalker();
await accessibility.enable();
info("Button front checks");
await checkNameAndRole(walker, "#button", a11yWalker, "Accessible Button");
info("Front with long name checks");
await checkNameAndRole(walker, "#h1", a11yWalker,
"Lorem ipsum dolor sit ame" + "\u2026" + "e et dolore magna aliqua.");
await accessibility.disable();
await waitForA11yShutdown();
await client.close();
gBrowser.removeCurrentTab();
});
/**
* A helper function for testing the accessible's displayed name and roles.
*
* @param {Object} walker
* The DOM walker.
* @param {String} querySelector
* The selector for the node to retrieve accessible from.
* @param {Object} a11yWalker
* The accessibility walker.
* @param {String} expectedName
* Expected string content for displaying the accessible's name.
* We are testing this in particular because name can be truncated.
*/
async function checkNameAndRole(walker, querySelector, a11yWalker, expectedName) {
const node = await walker.querySelector(walker.rootNode, querySelector);
const accessibleFront = await a11yWalker.getAccessibleFor(node);
const { name, role } = accessibleFront;
const onHighlightEvent = a11yWalker.once("highlighter-event");
await a11yWalker.highlightAccessible(accessibleFront);
const { options } = await onHighlightEvent;
is(options.name, name, "Accessible highlight has correct name option");
is(options.role, role, "Accessible highlight has correct role option");
is(`"${truncateString(name, MAX_STRING_LENGTH)}"`, `"${expectedName}"`,
"Accessible has correct displayed name.");
}

View file

@ -0,0 +1,162 @@
/* 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";
// Checks for the AccessibleHighlighter's and XULWindowHighlighter's infobar components.
add_task(async function() {
await BrowserTestUtils.withNewTab({
gBrowser,
url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
}, async function(browser) {
await ContentTask.spawn(browser, null, async function() {
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
const { HighlighterEnvironment } = require("devtools/server/actors/highlighters");
const { AccessibleHighlighter } = require("devtools/server/actors/highlighters/accessible");
const { XULWindowAccessibleHighlighter } = require("devtools/server/actors/highlighters/xul-accessible");
/**
* Get whether or not infobar container is hidden.
*
* @param {Object} infobar
* Accessible highlighter's infobar component.
* @return {String|null} If the infobar container is hidden.
*/
function isContainerHidden(infobar) {
return !!infobar.getElement("infobar-container").getAttribute("hidden");
}
/**
* Get name of accessible object.
*
* @param {Object} infobar
* Accessible highlighter's infobar component.
* @return {String} The text content of the infobar-name element.
*/
function getName(infobar) {
return infobar.getTextContent("infobar-name");
}
/**
* Get role of accessible object.
*
* @param {Object} infobar
* Accessible highlighter's infobar component.
* @return {String} The text content of the infobar-role element.
*/
function getRole(infobar) {
return infobar.getTextContent("infobar-role");
}
/**
* Checks for updated content for an infobar with valid bounds.
*
* @param {Object} infobar
* Accessible highlighter's infobar component.
* @param {Object} options
* Options to pass for the highlighter's show method.
* Available options:
* - {String} role
* Role value of the accessible.
* - {String} name
* Name value of the accessible.
* - {Boolean} shouldBeHidden
* If the infobar component should be hidden.
*/
function checkInfobar(infobar, { shouldBeHidden, role, name }) {
is(isContainerHidden(infobar), shouldBeHidden,
"Infobar's hidden state is correct.");
if (shouldBeHidden) {
return;
}
is(getRole(infobar), role, "infobarRole text content is correct");
is(getName(infobar), `"${name}"`, "infoBarName text content is correct");
}
/**
* Checks for updated content of an infobar with valid bounds.
*
* @param {Element} node
* Node to check infobar content on.
* @param {Object} highlighter
* Accessible highlighter.
*/
function testInfobar(node, highlighter) {
const infobar = highlighter.accessibleInfobar;
const bounds = {
x: 0,
y: 0,
w: 250,
h: 100,
};
info("Check that infobar is shown with valid bounds.");
highlighter.show(node, {
...bounds,
role: "button",
name: "Accessible Button"
});
checkInfobar(infobar, {
role: "button",
name: "Accessible Button",
shouldBeHidden: false
});
highlighter.hide();
info("Check that infobar is hidden after .hide() is called.");
checkInfobar(infobar, { shouldBeHidden: true });
info("Check to make sure content is updated with new options.");
highlighter.show(node, {
...bounds,
name: "Test link",
role: "link"
});
checkInfobar(infobar, {
name: "Test link",
role: "link",
shouldBeHidden: false
});
highlighter.hide();
}
// Start testing. First, create highlighter environment and initialize.
const env = new HighlighterEnvironment();
env.initFromWindow(content.window);
// Wait for loading highlighter environment content to complete before creating the
// highlighter.
await new Promise(resolve => {
const doc = env.document;
function onContentLoaded() {
if (doc.readyState === "interactive" || doc.readyState === "complete") {
resolve();
} else {
doc.addEventListener("DOMContentLoaded", onContentLoaded, { once: true });
}
}
onContentLoaded();
});
// Now, we can test the Infobar and XULWindowInfobar components with their
// respective highlighters.
const node = content.document.createElement("div");
content.document.body.append(node);
info("Checks for Infobar's show method");
const highlighter = new AccessibleHighlighter(env);
testInfobar(node, highlighter);
info("Checks for XULWindowInfobar's show method");
const xulWindowHighlighter = new XULWindowAccessibleHighlighter(env);
testInfobar(node, xulWindowHighlighter);
});
});
});

View file

@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
<button id="button">Accessible Button</button>
</body>
</html>

View file

@ -5,5 +5,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'css-logic.js'
'css-logic.js',
'utils.js'
)

View file

@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Truncate the string and add ellipsis to the middle of the string.
*/
function truncateString(str, maxLength) {
if (!str || str.length <= maxLength) {
return str;
}
return str.substring(0, Math.ceil(maxLength / 2)) +
"…" +
str.substring(str.length - Math.floor(maxLength / 2));
}
exports.truncateString = truncateString;

View file

@ -104,6 +104,10 @@ const accessibleWalkerSpec = generateActorSpec({
},
"picker-accessible-canceled": {
type: "pickerAccessibleCanceled"
},
"highlighter-event": {
type: "highlighter-event",
data: Arg(0, "json")
}
},

View file

@ -37,6 +37,7 @@
#include "nsPrintfCString.h"
#include "mozilla/Sprintf.h"
#include "nsGlobalWindow.h"
#include "nsReadableUtils.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/CustomElementRegistry.h"
@ -2781,34 +2782,16 @@ NonVoidByteStringToJsval(JSContext *cx, const nsACString &str,
return true;
}
template<typename T> static void
NormalizeUSVStringInternal(T& aString)
{
char16_t* start = aString.BeginWriting();
// Must use const here because we can't pass char** to UTF16CharEnumerator as
// it expects const char**. Unclear why this is illegal...
const char16_t* nextChar = start;
const char16_t* end = aString.Data() + aString.Length();
while (nextChar < end) {
uint32_t enumerated = UTF16CharEnumerator::NextChar(&nextChar, end);
if (enumerated == UCS2_REPLACEMENT_CHAR) {
int32_t lastCharIndex = (nextChar - start) - 1;
start[lastCharIndex] = static_cast<char16_t>(enumerated);
}
}
}
void
NormalizeUSVString(nsAString& aString)
{
NormalizeUSVStringInternal(aString);
EnsureUTF16Validity(aString);
}
void
NormalizeUSVString(binding_detail::FakeString& aString)
{
NormalizeUSVStringInternal(aString);
EnsureUTF16ValiditySpan(aString);
}
bool

View file

@ -10,6 +10,7 @@
#include "nsString.h"
#include "nsStringBuffer.h"
#include "mozilla/RefPtr.h"
#include "mozilla/Span.h"
namespace mozilla {
namespace dom {
@ -69,6 +70,8 @@ struct FakeString {
nsString::char_type* BeginWriting()
{
MOZ_ASSERT(!(mDataFlags & nsString::DataFlags::REFCOUNTED) ||
!nsStringBuffer::FromData(mData)->IsReadonly());
return mData;
}
@ -77,6 +80,16 @@ struct FakeString {
return mLength;
}
operator mozilla::Span<const nsString::char_type>() const
{
return mozilla::MakeSpan(Data(), Length());
}
operator mozilla::Span<nsString::char_type>()
{
return mozilla::MakeSpan(BeginWriting(), Length());
}
// Reserve space to write aLength chars, not including null-terminator.
bool SetLength(nsString::size_type aLength, mozilla::fallible_t const&) {
// Use mInlineStorage for small strings.

View file

@ -16,7 +16,6 @@ UNIFIED_SOURCES += [
'MockMediaResource.cpp',
'TestAudioBuffers.cpp',
'TestAudioCompactor.cpp',
'TestAudioDeviceEnumerator.cpp',
'TestAudioMixer.cpp',
'TestAudioPacketizer.cpp',
'TestAudioSegment.cpp',
@ -47,6 +46,11 @@ if CONFIG['MOZ_WEBM_ENCODER']:
'TestWebMWriter.cpp',
]
if CONFIG['MOZ_WEBRTC']:
UNIFIED_SOURCES += [
'TestAudioDeviceEnumerator.cpp',
]
TEST_HARNESS_FILES.gtest += [
'../test/gizmo-frag.mp4',
'../test/gizmo.mp4',

View file

@ -25,7 +25,8 @@ public:
virtual AudioBlockBuffer* AsAudioBlockBuffer() override { return this; };
float* ChannelData(uint32_t aChannel)
uint32_t ChannelsAllocated() const { return mChannelsAllocated; }
float* ChannelData(uint32_t aChannel) const
{
float* base = reinterpret_cast<float*>(((uintptr_t)(this + 1) + 15) & ~0x0F);
ASSERT_ALIGNED16(base);
@ -44,7 +45,7 @@ public:
}
void* m = operator new(size.value());
RefPtr<AudioBlockBuffer> p = new (m) AudioBlockBuffer();
RefPtr<AudioBlockBuffer> p = new (m) AudioBlockBuffer(aChannelCount);
NS_ASSERTION((reinterpret_cast<char*>(p.get() + 1) - reinterpret_cast<char*>(p.get())) % 4 == 0,
"AudioBlockBuffers should be at least 4-byte aligned");
return p.forget();
@ -59,7 +60,7 @@ public:
// Whether this is shared by any owners that are not downstream.
// Called only from owners with a reference that is not a downstream
// reference. Graph thread only.
bool HasLastingShares()
bool HasLastingShares() const
{
// mRefCnt is atomic and so reading its value is defined even when
// modifications may happen on other threads. mDownstreamRefCount is
@ -86,10 +87,12 @@ public:
}
private:
AudioBlockBuffer() {}
explicit AudioBlockBuffer(uint32_t aChannelsAllocated)
: mChannelsAllocated(aChannelsAllocated) {}
~AudioBlockBuffer() override { MOZ_ASSERT(mDownstreamRefCount == 0); }
nsAutoRefCnt mDownstreamRefCount;
const uint32_t mChannelsAllocated;
};
AudioBlock::~AudioBlock()
@ -143,11 +146,17 @@ AudioBlock::AllocateChannels(uint32_t aChannelCount)
if (mBufferIsDownstreamRef) {
// This is not our buffer to re-use.
ClearDownstreamMark();
} else if (mBuffer && ChannelCount() == aChannelCount) {
} else if (mBuffer) {
AudioBlockBuffer* buffer = mBuffer->AsAudioBlockBuffer();
if (buffer && !buffer->HasLastingShares()) {
if (buffer && !buffer->HasLastingShares() &&
buffer->ChannelsAllocated() >= aChannelCount) {
MOZ_ASSERT(mBufferFormat == AUDIO_FORMAT_FLOAT32);
// No need to allocate again.
uint32_t previousChannelCount = ChannelCount();
mChannelData.SetLength(aChannelCount);
for (uint32_t i = previousChannelCount; i < aChannelCount; ++i) {
mChannelData[i] = buffer->ChannelData(i);
}
mVolume = 1.0f;
return;
}

View file

@ -30,13 +30,64 @@ class ConvolverNodeEngine final : public AudioNodeEngine
public:
ConvolverNodeEngine(AudioNode* aNode, bool aNormalize)
: AudioNodeEngine(aNode)
, mLeftOverData(INT32_MIN)
, mSampleRate(0.0f)
, mUseBackgroundThreads(!aNode->Context()->IsOffline())
, mNormalize(aNormalize)
{
}
// Indicates how the right output channel is generated.
enum class RightConvolverMode {
// A right convolver is always used when there is more than one impulse
// response channel.
Always,
// With a single response channel, the mode may be either Direct or
// Difference. The decision on which to use is made when stereo input is
// received. Once the right convolver is in use, convolver state is
// suitable only for the selected mode, and so the mode cannot change
// until the right convolver contains only silent history.
//
// With Direct mode, each convolver processes a corresponding channel.
// This mode is selected when input is initially stereo or
// channelInterpretation is "discrete" at the time or starting the right
// convolver when input changes from non-silent mono to stereo.
Direct,
// Difference mode is selected if channelInterpretation is "speakers" at
// the time starting the right convolver when the input changes from mono
// to stereo.
//
// When non-silent input is initially mono, with a single response
// channel, the right output channel is not produced until input becomes
// stereo. Only a single convolver is used for mono processing. When
// stereo input arrives after mono input, output must be as if the mono
// signal remaining in the left convolver is up-mixed, but the right
// convolver has not been initialized with the history of the mono input.
// Copying the state of the left convolver into the right convolver is not
// desirable, because there is considerable state to copy, and the
// different convolvers are intended to process out of phase, which means
// that state from one convolver would not directly map to state in
// another convolver.
//
// Instead the distributive property of convolution is used to generate
// the right output channel using information in the left output channel.
// Using l and r to denote the left and right channel input signals, g the
// impulse response, and * convolution, the convolution of the right
// channel can be given by
//
// r * g = (l + (r - l)) * g
// = l * g + (r - l) * g
//
// The left convolver continues to process the left channel l to produce
// l * g. The right convolver processes the difference of input channel
// signals r - l to produce (r - l) * g. The outputs of the two
// convolvers are added to generate the right channel output r * g.
//
// The benefit of doing this is that the history of the r - l input for a
// "speakers" up-mixed mono signal is zero, and so an empty convolver
// already has exactly the right history for mixing the previous mono
// signal with the new stereo signal.
Difference
};
enum Parameters {
SAMPLE_RATE,
NORMALIZE
@ -73,81 +124,62 @@ public:
// Very large FFTs will have worse phase errors. Given these constraints 32768 is a good compromise.
const size_t MaxFFTSize = 32768;
mLeftOverData = INT32_MIN; // reset
// Reset.
mRemainingLeftOutput = INT32_MIN;
mRemainingRightOutput = 0;
mRemainingRightHistory = 0;
if (aBuffer.IsNull() || !mSampleRate) {
mReverb = nullptr;
return;
}
// Assume for now that convolution of channel difference is not required.
// Direct may change to Difference during processing.
mRightConvolverMode =
aBuffer.ChannelCount() == 1 ? RightConvolverMode::Direct
: RightConvolverMode::Always;
mReverb = new WebCore::Reverb(aBuffer, MaxFFTSize, mUseBackgroundThreads,
mNormalize, mSampleRate);
}
void AllocateReverbInput(const AudioBlock& aInput,
uint32_t aTotalChannelCount)
{
uint32_t inputChannelCount = aInput.ChannelCount();
MOZ_ASSERT(inputChannelCount <= aTotalChannelCount);
mReverbInput.AllocateChannels(aTotalChannelCount);
// Pre-multiply the input's volume
for (uint32_t i = 0; i < inputChannelCount; ++i) {
const float* src = static_cast<const float*>(aInput.mChannelData[i]);
float* dest = mReverbInput.ChannelFloatsForWrite(i);
AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest);
}
// Fill remaining channels with silence
for (uint32_t i = inputChannelCount; i < aTotalChannelCount; ++i) {
float* dest = mReverbInput.ChannelFloatsForWrite(i);
std::fill_n(dest, WEBAUDIO_BLOCK_SIZE, 0.0f);
}
}
void ProcessBlock(AudioNodeStream* aStream,
GraphTime aFrom,
const AudioBlock& aInput,
AudioBlock* aOutput,
bool* aFinished) override
{
if (!mReverb) {
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
return;
}
AudioBlock input = aInput;
if (aInput.IsNull()) {
if (mLeftOverData > 0) {
mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
input.AllocateChannels(1);
WriteZeroesToAudioBlock(&input, 0, WEBAUDIO_BLOCK_SIZE);
} else {
if (mLeftOverData != INT32_MIN) {
mLeftOverData = INT32_MIN;
aStream->ScheduleCheckForInactive();
RefPtr<PlayingRefChanged> refchanged =
new PlayingRefChanged(aStream, PlayingRefChanged::RELEASE);
aStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
refchanged.forget());
}
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
return;
}
} else {
if (aInput.mVolume != 1.0f) {
// Pre-multiply the input's volume
uint32_t numChannels = aInput.ChannelCount();
input.AllocateChannels(numChannels);
for (uint32_t i = 0; i < numChannels; ++i) {
const float* src = static_cast<const float*>(aInput.mChannelData[i]);
float* dest = input.ChannelFloatsForWrite(i);
AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest);
}
}
if (mLeftOverData <= 0) {
RefPtr<PlayingRefChanged> refchanged =
new PlayingRefChanged(aStream, PlayingRefChanged::ADDREF);
aStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
refchanged.forget());
}
mLeftOverData = mReverb->impulseResponseLength();
MOZ_ASSERT(mLeftOverData > 0);
}
aOutput->AllocateChannels(2);
mReverb->process(&input, aOutput);
}
bool* aFinished) override;
bool IsActive() const override
{
return mLeftOverData != INT32_MIN;
return mRemainingLeftOutput != INT32_MIN;
}
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
{
size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
amount += mReverbInput.SizeOfExcludingThis(aMallocSizeOf, false);
if (mReverb) {
amount += mReverb->sizeOfIncludingThis(aMallocSizeOf);
}
@ -161,13 +193,203 @@ public:
}
private:
// Keeping mReverbInput across process calls avoids unnecessary reallocation.
AudioBlock mReverbInput;
nsAutoPtr<WebCore::Reverb> mReverb;
int32_t mLeftOverData;
float mSampleRate;
// Tracks samples of the tail remaining to be output. INT32_MIN is a
// special value to indicate that the end of any previous tail has been
// handled.
int32_t mRemainingLeftOutput = INT32_MIN;
// mRemainingRightOutput and mRemainingRightHistory are only used when
// mRightOutputMode != Always. There is no special handling required at the
// end of tail times and so INT32_MIN is not used.
// mRemainingRightOutput tracks how much longer this node needs to continue
// to produce a right output channel.
int32_t mRemainingRightOutput = 0;
// mRemainingRightHistory tracks how much silent input would be required to
// drain the right convolver, which may sometimes be longer than the period
// a right output channel is required.
int32_t mRemainingRightHistory = 0;
float mSampleRate = 0.0f;
RightConvolverMode mRightConvolverMode = RightConvolverMode::Always;
bool mUseBackgroundThreads;
bool mNormalize;
};
static void
AddScaledLeftToRight(AudioBlock* aBlock, float aScale)
{
const float* left = static_cast<const float*>(aBlock->mChannelData[0]);
float* right = aBlock->ChannelFloatsForWrite(1);
AudioBlockAddChannelWithScale(left, aScale, right);
}
void
ConvolverNodeEngine::ProcessBlock(AudioNodeStream* aStream,
GraphTime aFrom,
const AudioBlock& aInput,
AudioBlock* aOutput,
bool* aFinished)
{
if (!mReverb) {
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
return;
}
uint32_t inputChannelCount = aInput.ChannelCount();
if (aInput.IsNull()) {
if (mRemainingLeftOutput > 0) {
mRemainingLeftOutput -= WEBAUDIO_BLOCK_SIZE;
AllocateReverbInput(aInput, 1); // floats for silence
} else {
if (mRemainingLeftOutput != INT32_MIN) {
mRemainingLeftOutput = INT32_MIN;
MOZ_ASSERT(mRemainingRightOutput <= 0);
MOZ_ASSERT(mRemainingRightHistory <= 0);
aStream->ScheduleCheckForInactive();
RefPtr<PlayingRefChanged> refchanged =
new PlayingRefChanged(aStream, PlayingRefChanged::RELEASE);
aStream->Graph()->
DispatchToMainThreadAfterStreamStateUpdate(refchanged.forget());
}
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
return;
}
} else {
if (mRemainingLeftOutput <= 0) {
RefPtr<PlayingRefChanged> refchanged =
new PlayingRefChanged(aStream, PlayingRefChanged::ADDREF);
aStream->Graph()->
DispatchToMainThreadAfterStreamStateUpdate(refchanged.forget());
}
// Use mVolume as a flag to detect whether AllocateReverbInput() gets
// called.
mReverbInput.mVolume = 0.0f;
// Special handling of input channel count changes is used when there is
// only a single impulse response channel. See RightConvolverMode.
if (mRightConvolverMode != RightConvolverMode::Always) {
ChannelInterpretation channelInterpretation =
aStream->GetChannelInterpretation();
if (inputChannelCount == 2) {
if (mRemainingRightHistory <= 0) {
// Will start the second convolver. Choose to convolve the right
// channel directly if there is no left tail to up-mix or up-mixing
// is "discrete".
mRightConvolverMode =
(mRemainingLeftOutput <= 0 ||
channelInterpretation == ChannelInterpretation::Discrete) ?
RightConvolverMode::Direct : RightConvolverMode::Difference;
}
// The extra WEBAUDIO_BLOCK_SIZE is subtracted below.
mRemainingRightOutput =
mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE;
mRemainingRightHistory = mRemainingRightOutput;
if (mRightConvolverMode == RightConvolverMode::Difference) {
AllocateReverbInput(aInput, 2);
// Subtract left from right.
AddScaledLeftToRight(&mReverbInput, -1.0f);
}
} else if (mRemainingRightHistory > 0) {
// There is one channel of input, but a second convolver also
// requires input. Up-mix appropriately for the second convolver.
if ((mRightConvolverMode == RightConvolverMode::Difference) ^
(channelInterpretation == ChannelInterpretation::Discrete)) {
MOZ_ASSERT(
(mRightConvolverMode == RightConvolverMode::Difference &&
channelInterpretation == ChannelInterpretation::Speakers) ||
(mRightConvolverMode == RightConvolverMode::Direct &&
channelInterpretation == ChannelInterpretation::Discrete));
// The state is one of the following combinations:
// 1) Difference and speakers.
// Up-mixing gives r = l.
// The input to the second convolver is r - l.
// 2) Direct and discrete.
// Up-mixing gives r = 0.
// The input to the second convolver is r.
//
// In each case the input for the second convolver is silence, which
// will drain the convolver.
AllocateReverbInput(aInput, 2);
} else {
if (channelInterpretation == ChannelInterpretation::Discrete) {
MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Difference);
// channelInterpretation has changed since the second convolver
// was added. "discrete" up-mixing of input would produce a
// silent right channel r = 0, but the second convolver needs
// r - l for RightConvolverMode::Difference.
AllocateReverbInput(aInput, 2);
AddScaledLeftToRight(&mReverbInput, -1.0f);
} else {
MOZ_ASSERT(channelInterpretation ==
ChannelInterpretation::Speakers);
MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Direct);
// The Reverb will essentially up-mix the single input channel by
// feeding it into both convolvers.
}
// The second convolver does not have silent input, and so it will
// not drain. It will need to continue processing up-mixed input
// because the next input block may be stereo, which would be mixed
// with the signal remaining in the convolvers.
// The extra WEBAUDIO_BLOCK_SIZE is subtracted below.
mRemainingRightHistory =
mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE;
}
}
}
if (mReverbInput.mVolume == 0.0f) { // not yet set
if (aInput.mVolume != 1.0f) {
AllocateReverbInput(aInput, inputChannelCount); // pre-multiply
} else {
mReverbInput = aInput;
}
}
mRemainingLeftOutput = mReverb->impulseResponseLength();
MOZ_ASSERT(mRemainingLeftOutput > 0);
}
// "The ConvolverNode produces a mono output only in the single case where
// there is a single input channel and a single-channel buffer."
uint32_t outputChannelCount = 2;
uint32_t reverbOutputChannelCount = 2;
if (mRightConvolverMode != RightConvolverMode::Always) {
// When the input changes from stereo to mono, the output continues to be
// stereo for the length of the tail time, during which the two channels
// may differ.
if (mRemainingRightOutput > 0) {
MOZ_ASSERT(mRemainingRightHistory > 0);
mRemainingRightOutput -= WEBAUDIO_BLOCK_SIZE;
} else {
outputChannelCount = 1;
}
// The second convolver keeps processing until it drains.
if (mRemainingRightHistory > 0) {
mRemainingRightHistory -= WEBAUDIO_BLOCK_SIZE;
} else {
reverbOutputChannelCount = 1;
}
}
// If there are two convolvers, then they each need an output buffer, even
// if the second convolver is only processing to keep history of up-mixed
// input.
aOutput->AllocateChannels(reverbOutputChannelCount);
mReverb->process(&mReverbInput, aOutput);
if (mRightConvolverMode == RightConvolverMode::Difference &&
outputChannelCount == 2) {
// Add left to right.
AddScaledLeftToRight(aOutput, 1.0f);
} else {
// Trim if outputChannelCount < reverbOutputChannelCount
aOutput->mChannelData.TruncateLength(outputChannelCount);
}
}
ConvolverNode::ConvolverNode(AudioContext* aContext)
: AudioNode(aContext,
2,

View file

@ -118,11 +118,17 @@ skip-if = (android_version == '18' && debug) # bug 1158417
[test_convolverNode.html]
[test_convolverNode_mono_mono.html]
[test_convolverNodeChannelCount.html]
[test_convolverNodeChannelInterpretationChanges.html]
[test_convolverNodeDelay.html]
[test_convolverNodeFiniteInfluence.html]
[test_convolverNodeNormalization.html]
[test_convolverNodePassThrough.html]
[test_convolverNodeWithGain.html]
[test_convolver-upmixing-1-channel-response.html]
# This is a copy of
# testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html,
# but WPT are not run with ASan or Android builds.
skip-if = !asan && toolkit != android
[test_currentTime.html]
[test_decodeAudioDataOnDetachedBuffer.html]
[test_decodeAudioDataPromise.html]

View file

@ -0,0 +1,143 @@
<!DOCTYPE html>
<title>Test that up-mixing signals in ConvolverNode processing is linear</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
const EPSILON = 3.0 * Math.pow(2, -22);
// sampleRate is a power of two so that delay times are exact in base-2
// floating point arithmetic.
const SAMPLE_RATE = 32768;
// Length of stereo convolver input in frames (arbitrary):
const STEREO_FRAMES = 256;
// Length of mono signal in frames. This is more than two blocks to ensure
// that at least one block will be mono, even if interpolation in the
// DelayNode means that stereo is output one block earlier and later than
// if frames are delayed without interpolation.
const MONO_FRAMES = 384;
// Length of response buffer:
const RESPONSE_FRAMES = 256;
function test_linear_upmixing(channelInterpretation, initial_mono_frames)
{
let stereo_input_end = initial_mono_frames + STEREO_FRAMES;
// Total length:
let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES;
// The first two channels contain signal where some up-mixing occurs
// internally to a ConvolverNode when a stereo signal is added and removed.
// The last two channels are expected to contain the same signal, but mono
// and stereo signals are convolved independently before up-mixing the mono
// output to mix with the stereo output.
let context = new OfflineAudioContext({numberOfChannels: 4,
length: length,
sampleRate: SAMPLE_RATE});
let response = new AudioBuffer({numberOfChannels: 1,
length: RESPONSE_FRAMES,
sampleRate: context.sampleRate});
// Two stereo channel splitters will collect test and reference outputs.
let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
destinationMerger.connect(context.destination);
let testSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
let referenceSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
testSplitter.connect(destinationMerger, 0, 0);
testSplitter.connect(destinationMerger, 1, 1);
referenceSplitter.connect(destinationMerger, 0, 2);
referenceSplitter.connect(destinationMerger, 1, 3);
// A GainNode mixes reference stereo and mono signals because up-mixing
// cannot be performed at a channel splitter.
let referenceGain = new GainNode(context);
referenceGain.connect(referenceSplitter);
referenceGain.channelInterpretation = channelInterpretation;
// The impulse response for convolution contains two impulses so as to test
// effects in at least two processing blocks.
response.getChannelData(0)[0] = 0.5;
response.getChannelData(0)[response.length - 1] = 0.5;
let testConvolver = new ConvolverNode(context, {disableNormalization: true,
buffer: response});
testConvolver.channelInterpretation = channelInterpretation;
let referenceMonoConvolver = new ConvolverNode(context,
{disableNormalization: true,
buffer: response});
let referenceStereoConvolver = new ConvolverNode(context,
{disableNormalization: true,
buffer: response});
// No need to set referenceStereoConvolver.channelInterpretation because
// input is either silent or stereo.
testConvolver.connect(testSplitter);
// Mix reference convolver output.
referenceMonoConvolver.connect(referenceGain);
referenceStereoConvolver.connect(referenceGain);
// The DelayNode initially has a single channel of silence, which is used to
// switch the stereo signal in and out. The output of the delay node is
// first mono silence (if there is a non-zero initial_mono_frames), then
// stereo, then mono silence, and finally stereo again. maxDelayTime is
// used to generate the middle mono silence period from the initial silence
// in the DelayNode and then generate the final period of stereo from its
// initial input.
let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate;
let delay =
new DelayNode(context,
{maxDelayTime: maxDelayTime,
delayTime: initial_mono_frames / context.sampleRate});
// Schedule an increase in the delay to return to mono silence.
delay.delayTime.setValueAtTime(maxDelayTime,
stereo_input_end / context.sampleRate);
delay.connect(testConvolver);
delay.connect(referenceStereoConvolver);
let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
stereoMerger.connect(delay);
// Three independent signals
let monoSignal = new OscillatorNode(context, {frequency: 440});
let leftSignal = new OscillatorNode(context, {frequency: 450});
let rightSignal = new OscillatorNode(context, {frequency: 460});
monoSignal.connect(testConvolver);
monoSignal.connect(referenceMonoConvolver);
leftSignal.connect(stereoMerger, 0, 0);
rightSignal.connect(stereoMerger, 0, 1);
monoSignal.start();
leftSignal.start();
rightSignal.start();
return context.startRendering().
then((buffer) => {
let maxDiff = -1.0;
let frameIndex = 0;
let channelIndex = 0;
for (let c = 0; c < 2; ++c) {
let testOutput = buffer.getChannelData(0 + c);
let referenceOutput = buffer.getChannelData(2 + c);
for (var i = 0; i < buffer.length; ++i) {
var diff = Math.abs(testOutput[i] - referenceOutput[i]);
if (diff > maxDiff) {
maxDiff = diff;
frameIndex = i;
channelIndex = c;
}
}
}
assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
buffer.getChannelData(2 + channelIndex)[frameIndex],
EPSILON,
`output at ${frameIndex} ` +
`in channel ${channelIndex}` );
});
}
promise_test(() => test_linear_upmixing("speakers", MONO_FRAMES),
"speakers, initially mono");
promise_test(() => test_linear_upmixing("discrete", MONO_FRAMES),
"discrete");
// Gecko uses a separate path for "speakers" up-mixing when the convolver's
// first input is stereo, so test that separately.
promise_test(() => test_linear_upmixing("speakers", 0),
"speakers, initially stereo");
</script>

View file

@ -0,0 +1,169 @@
<!DOCTYPE html>
<title>Test up-mixing in ConvolverNode after ChannelInterpretation change</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
// This test is not in wpt because it requires that multiple changes to the
// nodes in an AudioContext during a single event will be processed by the
// audio thread in a single transaction. Gecko provides that, but this is not
// currently required by the Web Audio API.
const EPSILON = Math.pow(2, -23);
// sampleRate is a power of two so that delay times are exact in base-2
// floating point arithmetic.
const SAMPLE_RATE = 32768;
// Length of initial mono signal in frames, if the test has an initial mono
// signal. This is more than one block to ensure that at least one block
// will be mono, even if interpolation in the DelayNode means that stereo is
// output one block earlier than if frames are delayed without interpolation.
const MONO_FRAMES = 256;
// Length of response buffer. This is greater than 1 to ensure that the
// convolver has stereo output at least one block after stereo input is
// disconnected.
const RESPONSE_FRAMES = 2;
function test_interpretation_change(t, initialInterpretation, initialMonoFrames)
{
let context = new AudioContext({sampleRate: SAMPLE_RATE});
// Three independent signals. These are constant so that results are
// independent of the timing of the `ended` event.
let monoOffset = 0.25
let monoSource = new ConstantSourceNode(context, {offset: monoOffset});
let leftOffset = 0.125;
let rightOffset = 0.5;
let leftSource = new ConstantSourceNode(context, {offset: leftOffset});
let rightSource = new ConstantSourceNode(context, {offset: rightOffset});
monoSource.start();
leftSource.start();
rightSource.start();
let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
leftSource.connect(stereoMerger, 0, 0);
rightSource.connect(stereoMerger, 0, 1);
// The DelayNode initially has a single channel of silence, and so the
// output of the delay node is first mono silence (if there is a non-zero
// initialMonoFrames), then stereo. In Gecko, this triggers a convolver
// configuration that is different for different channelInterpretations.
let delay =
new DelayNode(context,
{maxDelayTime: MONO_FRAMES / context.sampleRate,
delayTime: initialMonoFrames / context.sampleRate});
stereoMerger.connect(delay);
// Two convolvers with the same impulse response. The test convolver will
// process a mix of stereo and mono signals. The reference convolver will
// always process stereo, including the up-mixed mono signal.
let response = new AudioBuffer({numberOfChannels: 1,
length: RESPONSE_FRAMES,
sampleRate: context.sampleRate});
response.getChannelData(0)[response.length - 1] = 1;
let testConvolver = new ConvolverNode(context,
{disableNormalization: true,
buffer: response});
testConvolver.channelInterpretation = initialInterpretation;
let referenceConvolver = new ConvolverNode(context,
{disableNormalization: true,
buffer: response});
// No need to set referenceConvolver.channelInterpretation because
// input is always stereo, due to up-mixing at gain node.
let referenceMixer = new GainNode(context);
referenceMixer.channelCount = 2;
referenceMixer.channelCountMode = "explicit";
referenceMixer.channelInterpretation = initialInterpretation;
referenceMixer.connect(referenceConvolver);
delay.connect(testConvolver);
delay.connect(referenceMixer);
monoSource.connect(testConvolver);
monoSource.connect(referenceMixer);
// A timer sends 'ended' when the convolvers are known to be processing
// stereo.
let timer = new ConstantSourceNode(context);
timer.start();
timer.stop((initialMonoFrames + 1) / context.sampleRate);
timer.onended = t.step_func(() => {
let changedInterpretation =
initialInterpretation == "speakers" ? "discrete" : "speakers";
// Switch channelInterpretation in test and reference paths.
testConvolver.channelInterpretation = changedInterpretation;
referenceMixer.channelInterpretation = changedInterpretation;
// Disconnect the stereo input from both test and reference convolvers.
// The disconnected convolvers will continue to output stereo for at least
// one frame. The test convolver will up-mix its mono input into its two
// buffers.
delay.disconnect();
// Capture the outputs in a script processor.
//
// The first two channels contain signal where some up-mixing occurs
// internally to the test convolver.
//
// The last two channels are expected to contain the same signal, but
// up-mixing was performed at a GainNode prior to convolution.
//
// Two stereo splitters will collect test and reference outputs.
let testSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
let referenceSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
testConvolver.connect(testSplitter);
referenceConvolver.connect(referenceSplitter);
let outputMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
testSplitter.connect(outputMerger, 0, 0);
testSplitter.connect(outputMerger, 1, 1);
referenceSplitter.connect(outputMerger, 0, 2);
referenceSplitter.connect(outputMerger, 1, 3);
let processor = context.createScriptProcessor(256, 4, 0);
outputMerger.connect(processor);
processor.onaudioprocess = t.step_func_done((e) => {
e.target.onaudioprocess = null;
outputMerger.disconnect();
// The test convolver output is stereo for the first block.
let length = 128;
let buffer = e.inputBuffer;
let maxDiff = -1.0;
let frameIndex = 0;
let channelIndex = 0;
for (let c = 0; c < 2; ++c) {
let testOutput = buffer.getChannelData(0 + c);
let referenceOutput = buffer.getChannelData(2 + c);
for (var i = 0; i < length; ++i) {
var diff = Math.abs(testOutput[i] - referenceOutput[i]);
if (diff > maxDiff) {
maxDiff = diff;
frameIndex = i;
channelIndex = c;
}
}
}
assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
buffer.getChannelData(2 + channelIndex)[frameIndex],
EPSILON,
`output at ${frameIndex} ` +
`in channel ${channelIndex}` );
});
});
}
async_test((t) => test_interpretation_change(t, "speakers", MONO_FRAMES),
"speakers to discrete, initially mono");
async_test((t) => test_interpretation_change(t, "discrete", MONO_FRAMES),
"discrete to speakers");
// Gecko uses a separate path for "speakers" initial up-mixing when the
// convolver's first input is stereo, so test that separately.
async_test((t) => test_interpretation_change(t, "speakers", 0),
"speakers to discrete, initially stereo");
</script>

View file

@ -12,7 +12,7 @@
Exposed=(ServiceWorker)]
interface FetchEvent : ExtendableEvent {
[SameObject] readonly attribute Request request;
readonly attribute DOMString? clientId;
readonly attribute DOMString clientId;
readonly attribute boolean isReload;
[Throws]
@ -21,6 +21,6 @@ interface FetchEvent : ExtendableEvent {
dictionary FetchEventInit : EventInit {
required Request request;
DOMString? clientId = null;
DOMString clientId = "";
boolean isReload = false;
};

View file

@ -618,6 +618,16 @@ pub unsafe extern "C" fn encoding_mem_is_str_latin1(buffer: *const u8, len: usiz
))
}
#[no_mangle]
pub unsafe extern "C" fn encoding_mem_utf16_valid_up_to(buffer: *const u16, len: usize) -> usize {
encoding_rs::mem::utf16_valid_up_to(::std::slice::from_raw_parts(buffer, len))
}
#[no_mangle]
pub unsafe extern "C" fn encoding_mem_ensure_utf16_validity(buffer: *mut u16, len: usize) {
encoding_rs::mem::ensure_utf16_validity(::std::slice::from_raw_parts_mut(buffer, len));
}
#[no_mangle]
pub unsafe extern "C" fn encoding_mem_convert_utf16_to_latin1_lossy(
src: *const u16,

View file

@ -10,7 +10,7 @@ fuzzy-if(winWidget&&!layersGPUAccelerated,0-142,0-276) == positioned-container-1
== relpos-legend-2.html relpos-legend-2-ref.html
== relpos-legend-3.html relpos-legend-3-ref.html
== relpos-legend-4.html relpos-legend-4-ref.html
fuzzy-if(webrender,17-19,1221-2452) == sticky-legend-1.html sticky-legend-1-ref.html
fuzzy-if(webrender,17-19,1217-2452) == sticky-legend-1.html sticky-legend-1-ref.html
fuzzy-if(skiaContent,0-1,0-40768) == abs-pos-child-sizing.html abs-pos-child-sizing-ref.html
== overflow-hidden.html overflow-hidden-ref.html
== legend-rtl.html legend-rtl-ref.html

View file

@ -12,8 +12,8 @@ fieldset { height: 100px; width: 100px; }
.v-rl .a { width: 40%; background: #eee; }
.v-rl .b { width: 60%; background: #ddd; }
.ltr fieldset, .rtl fieldset { margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
.v-rl fieldset { margin: 2px 0; padding: 0.625em 0.35em 0.625em 0.75em; }
.ltr fieldset, .rtl fieldset { margin: 0 2px; padding: 0.35em 0.75em 0.625em; }
.v-rl fieldset { margin: 2px 0; padding: 0.75em 0.35em 0.75em 0.625em; }
.ltr legend, .rtl legend { padding: 0 2px; }
.v-rl legend { padding: 2px 0; }

View file

@ -70,9 +70,9 @@ fieldset {
margin-inline-start: 2px;
margin-inline-end: 2px;
padding-block-start: 0.35em;
padding-block-end: 0.75em;
padding-inline-start: 0.625em;
padding-inline-end: 0.625em;
padding-block-end: 0.625em;
padding-inline-start: 0.75em;
padding-inline-end: 0.75em;
border: 2px groove ThreeDLightShadow;
min-width: -moz-min-content;
}

View file

@ -932,6 +932,12 @@ TransceiverImpl::UpdateVideoConduit()
return rv;
}
if (configs.values.empty()) {
MOZ_MTLOG(ML_INFO, mPCHandle << "[" << mMid << "]: " << __FUNCTION__ <<
" No codecs were negotiated (send).");
return NS_OK;
}
auto error = conduit->ConfigureSendMediaCodec(configs.values[0]);
if (error) {
MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: " << __FUNCTION__ <<

View file

@ -0,0 +1,220 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko;
import android.view.Menu;
import android.view.MenuItem;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mozilla.gecko.util.GeckoBundle;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mozilla.gecko.toolbar.PageActionLayout.PageAction;
import static org.mozilla.gecko.toolbar.PageActionLayout.PageActionLayoutDelegate;
@RunWith(RobolectricTestRunner.class)
public class TestAddonUICache {
private AddonUICache mAddonUICache;
private @Mock Menu mMockMenu;
private @Mock PageActionLayoutDelegate mMockPalDelegate;
private @Captor ArgumentCaptor<List<PageAction>> palCaptor;
private static final MessageGenerator addonMessage = new AddonMenuMessageGenerator();
private static final MessageGenerator pageActionMessage = new PageActionMessageGenerator();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mMockMenu.add(anyInt(), anyInt(), anyInt(), anyString())).thenReturn(mock(MenuItem.class));
mAddonUICache = AddonUICache.getInstance();
mAddonUICache.init();
}
@After
public void tearDown() {
mAddonUICache.reset();
}
@Test
public void testMenuMessageDirectForward() {
mAddonUICache.onCreateOptionsMenu(mMockMenu);
verify(mMockMenu, never()).add(anyInt(), anyInt(), anyInt(), anyString());
Map<String, GeckoBundle> sentMessages = sendAddonMessages("Menu:Add",
addonMessage);
ArgumentCaptor<String> menuLabel = ArgumentCaptor.forClass(String.class);
verify(mMockMenu, times(4)).add(anyInt(), anyInt(), anyInt(), menuLabel.capture());
List<String> expectedLabels = new ArrayList<>(sentMessages.keySet());
Assert.assertEquals(expectedLabels, menuLabel.getAllValues());
}
@Test
public void testMenuMessageStoreForward() {
Map<String, GeckoBundle> sentMessages = sendAddonMessages("Menu:Add",
addonMessage);
mAddonUICache.onCreateOptionsMenu(mMockMenu);
ArgumentCaptor<String> menuLabel = ArgumentCaptor.forClass(String.class);
verify(mMockMenu, times(4)).add(anyInt(), anyInt(), anyInt(), menuLabel.capture());
List<String> expectedLabels = new ArrayList<>(sentMessages.keySet());
Assert.assertEquals(expectedLabels, menuLabel.getAllValues());
}
@Test
public void testMenuMessageStoreRemoveForward() {
Map<String, GeckoBundle> sentMessages = sendAddonMessages("Menu:Add",
addonMessage);
sendAddonMessages("Menu:Remove", new ArrayList<>(sentMessages.values()));
mAddonUICache.onCreateOptionsMenu(mMockMenu);
verify(mMockMenu, never()).add(anyInt(), anyInt(), anyInt(), anyString());
}
@Test
public void testResolvedPageActionListSaving() {
final List<PageAction> pageActionList = new ArrayList<>();
mAddonUICache.removePageActionLayoutDelegate(pageActionList);
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
verify(mMockPalDelegate).setCachedPageActions(palCaptor.capture());
Assert.assertEquals(pageActionList, palCaptor.getValue());
}
@Test
public void testPageActionMessageDirectForward() {
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
verify(mMockPalDelegate, never()).addPageAction(any(GeckoBundle.class));
verify(mMockPalDelegate, never()).removePageAction(any(GeckoBundle.class));
Map<String, GeckoBundle> sentMessages = sendAddonMessages("PageActions:Add",
pageActionMessage);
ArgumentCaptor<GeckoBundle> messages = ArgumentCaptor.forClass(GeckoBundle.class);
verify(mMockPalDelegate, times(4)).addPageAction(messages.capture());
List<GeckoBundle> expectedMessages = new ArrayList<>(sentMessages.values());
Assert.assertEquals(expectedMessages, messages.getAllValues());
sendAddonMessages("PageActions:Remove", new ArrayList<>(sentMessages.values()));
messages = ArgumentCaptor.forClass(GeckoBundle.class);
verify(mMockPalDelegate, times(4)).removePageAction(messages.capture());
Assert.assertEquals(expectedMessages, messages.getAllValues());
}
@Test
public void testPageActionMessageStoreForward() {
Map<String, GeckoBundle> sentMessages = sendAddonMessages("PageActions:Add",
pageActionMessage);
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
ArgumentCaptor<GeckoBundle> messages = ArgumentCaptor.forClass(GeckoBundle.class);
verify(mMockPalDelegate, times(4)).addPageAction(messages.capture());
List<GeckoBundle> expectedMessages = new ArrayList<>(sentMessages.values());
Assert.assertEquals(expectedMessages, messages.getAllValues());
verify(mMockPalDelegate, never()).removePageAction(any(GeckoBundle.class));
}
@Test
public void testPageActionMessageStoreRemoveForward() {
Map<String, GeckoBundle> sentMessages = sendAddonMessages("PageActions:Add",
pageActionMessage);
sendAddonMessages("PageActions:Remove", new ArrayList<>(sentMessages.values()));
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
verify(mMockPalDelegate, never()).addPageAction(any(GeckoBundle.class));
verify(mMockPalDelegate, never()).removePageAction(any(GeckoBundle.class));
}
@Test
public void testResolvedPageActionListSavingRemoval() {
final List<PageAction> pageActionList = new ArrayList<>();
final GeckoBundle palMessage = pageActionMessage.getMessage("Frob widget");
final PageAction pageAction = new PageAction(palMessage.getString("id"),
palMessage.getString("title"), null, null, false);
pageActionList.add(pageAction);
mAddonUICache.removePageActionLayoutDelegate(pageActionList);
sendAddonMessage("PageActions:Remove", palMessage);
mAddonUICache.setPageActionLayoutDelegate(mMockPalDelegate);
verify(mMockPalDelegate).setCachedPageActions(palCaptor.capture());
Assert.assertEquals(pageActionList, palCaptor.getValue());
Assert.assertTrue(pageActionList.isEmpty());
}
private Map<String, GeckoBundle> sendAddonMessages(String event, MessageGenerator generator) {
Map<String, GeckoBundle> sentMessages = new LinkedHashMap<>();
for (int i = 0; i < 4; i++) {
String label = "Menu " + i;
GeckoBundle message = generator.getMessage(label);
sendAddonMessage(event, message);
sentMessages.put(label, message);
}
return sentMessages;
}
private void sendAddonMessages(String event, List<GeckoBundle> messages) {
for (GeckoBundle message : messages) {
sendAddonMessage(event, message);
}
}
private void sendAddonMessage(String event, GeckoBundle message) {
mAddonUICache.handleMessage(event, message, null);
}
private interface MessageGenerator {
GeckoBundle getMessage(String label);
}
private static class AddonMenuMessageGenerator implements MessageGenerator {
@Override
public GeckoBundle getMessage(String label) {
GeckoBundle message = new GeckoBundle(2);
message.putString("name", label);
message.putString("uuid", "{" + UUID.randomUUID().toString() + "}");
return message;
}
}
private static class PageActionMessageGenerator implements MessageGenerator {
@Override
public GeckoBundle getMessage(String label) {
GeckoBundle message = new GeckoBundle(2);
message.putString("title", label);
message.putString("id", "{" + UUID.randomUUID().toString() + "}");
return message;
}
}
}

View file

@ -0,0 +1,355 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* vim: ts=4 sw=4 expandtab:
* 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.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.mozilla.gecko.toolbar.PageActionLayout.PageAction;
import static org.mozilla.gecko.toolbar.PageActionLayout.PageActionLayoutDelegate;
/**
* For certain UI items added by add-ons or other JS/Gecko code, Gecko notifies us whenever an item
* is added, changed or removed. Since we must not miss any of these notifications and need to re-
* member the current list of active UI items even if e.g. we're in background and our activities
* have been destroyed, we need a class whose lifetime matches (or even exceeds) that of Gecko.
*
* This class fulfills this purpose - it will be initialised early during app startup and just like
* Gecko, once initialised it will remain alive until the OS kills our app.
*
* After initialisation, we will start listening for the appropriate EventDispatcher messages from
* Gecko and maintain an internal list of UI items dynamically added by Gecko.
* In addition, for each class of UI elements an appropriate API will be provided through which the
* intended final consumer can make use of that list in order to actually show those elements in the
* UI.
*/
public class AddonUICache implements BundleEventListener {
private static final String LOGTAG = "GeckoAddonUICache";
private static final int GECKO_TOOLS_MENU_ID = -1;
// When changing this, make sure to adjust NativeWindow.toolsMenuID in browser.js, too.
private static final String GECKO_TOOLS_MENU_UUID = "{115b9308-2023-44f1-a4e9-3e2197669f07}";
private static final int ADDON_MENU_OFFSET = 1000;
private static class MenuItemInfo {
public int id;
public String uuid;
public String label;
public boolean checkable;
public boolean checked;
public boolean enabled = true;
public boolean visible = true;
public int parent;
}
private static final AddonUICache instance = new AddonUICache();
private final Map<String, MenuItemInfo> mAddonMenuItemsCache = new LinkedHashMap<>();
private int mAddonMenuNextID = ADDON_MENU_OFFSET;
private Menu mMenu;
// A collection of PageAction messages that are still pending transformation into
// a full PageAction - most importantly transformation of the image data URI
// into a Drawable.
private final Map<String, GeckoBundle> mPendingPageActionQueue = new LinkedHashMap<>();
// A collection of PageActions ready for immediate usage.
private List<PageAction> mResolvedPageActionCache;
private PageActionLayoutDelegate mPageActionDelegate;
private boolean mInitialized;
public static AddonUICache getInstance() {
return instance;
}
private AddonUICache() { }
public void init() {
if (mInitialized) {
return;
}
EventDispatcher.getInstance().registerUiThreadListener(this,
"Menu:Add",
"Menu:Update",
"Menu:Remove",
"PageActions:Add",
"PageActions:Remove",
null);
mInitialized = true;
}
@VisibleForTesting
/* package, intended private */ void reset() {
mAddonMenuItemsCache.clear();
mAddonMenuNextID = ADDON_MENU_OFFSET;
mMenu = null;
mPendingPageActionQueue.clear();
mResolvedPageActionCache = null;
mPageActionDelegate = null;
}
@Override
public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
switch (event) {
case "Menu:Add":
final MenuItemInfo info = new MenuItemInfo();
info.label = message.getString("name");
if (TextUtils.isEmpty(info.label)) {
Log.e(LOGTAG, "Invalid menu item name");
return;
}
info.id = mAddonMenuNextID++;
info.uuid = message.getString("uuid");
info.checked = message.getBoolean("checked", false);
info.enabled = message.getBoolean("enabled", true);
info.visible = message.getBoolean("visible", true);
info.checkable = message.getBoolean("checkable", false);
final String parentUUID = message.getString("parent");
if (GECKO_TOOLS_MENU_UUID.equals(parentUUID)) {
info.parent = GECKO_TOOLS_MENU_ID;
} else if (!TextUtils.isEmpty(parentUUID)) {
MenuItemInfo parent = mAddonMenuItemsCache.get(parentUUID);
if (parent != null) {
info.parent = parent.id;
}
}
addAddonMenuItem(info);
break;
case "Menu:Remove":
removeAddonMenuItem(message.getString("uuid"));
break;
case "Menu:Update":
updateAddonMenuItem(message.getString("uuid"),
message.getBundle("options"));
break;
case "PageActions:Add":
if (mPageActionDelegate != null) {
mPageActionDelegate.addPageAction(message);
} else {
mPendingPageActionQueue.put(message.getString("id"), message);
}
break;
case "PageActions:Remove":
if (mPageActionDelegate != null) {
mPageActionDelegate.removePageAction(message);
} else {
final String id = message.getString("id");
mPendingPageActionQueue.remove(id);
if (mResolvedPageActionCache != null) {
PageAction.removeFromList(mResolvedPageActionCache, id);
}
}
break;
}
}
/**
* If a list of {@link PageAction PageActions} has previously been provided in
* {@link AddonUICache#removePageActionLayoutDelegate}, it will be transferred back to the
* {@link PageActionLayoutDelegate}.
* In addition, any <code>GeckoBundles</code> containing <code>PageAction</code> messages that
* arrived while no delegate was available will now be transmitted.
* <p>
* Following this, any <code>PageAction</code> messages that arrive will be forwarded
* immediately to the provided delegate.
*/
public void setPageActionLayoutDelegate(final @NonNull PageActionLayoutDelegate newDelegate) {
newDelegate.setCachedPageActions(mResolvedPageActionCache);
mResolvedPageActionCache = null;
for (GeckoBundle pageActionMessage : mPendingPageActionQueue.values()) {
newDelegate.addPageAction(pageActionMessage);
}
mPendingPageActionQueue.clear();
mPageActionDelegate = newDelegate;
}
/**
* Clears the current PageActionDelegate and optionally takes a list of PageActions
* that will be stored, e.g. if the class that provided the delegate is going away.
*
* In addition, all PageAction EventDispatcher messages that arrive while no delegate is
* available will be stored for later retrieval.
*/
public void removePageActionLayoutDelegate(final @Nullable List<PageAction> pageActionsToCache) {
mPageActionDelegate = null;
mResolvedPageActionCache = pageActionsToCache;
}
/**
* Starts handling add-on menu items for the given {@link Menu} and also adds any
* menu items that have already been cached.
*/
public void onCreateOptionsMenu(Menu menu) {
mMenu = menu;
// Add add-on menu items, if any exist.
if (mMenu != null) {
for (MenuItemInfo item : mAddonMenuItemsCache.values()) {
addAddonMenuItemToMenu(mMenu, item);
}
}
}
/**
* Clears the reference to the Menu passed in {@link AddonUICache#onCreateOptionsMenu}.
* <p>
* Note: Any {@link MenuItem MenuItem(s)} previously added by this class are <i>not</i> removed.
*/
public void onDestroyOptionsMenu() {
mMenu = null;
}
/**
* Adds an addon menu item/webextension browser action to the menu.
*/
private void addAddonMenuItem(final MenuItemInfo info) {
mAddonMenuItemsCache.put(info.uuid, info);
if (mMenu == null) {
return;
}
addAddonMenuItemToMenu(mMenu, info);
}
/**
* Removes an addon menu item/webextension browser action from the menu by its UUID.
*/
private void removeAddonMenuItem(String uuid) {
// Remove add-on menu item from cache, if available.
final MenuItemInfo item = mAddonMenuItemsCache.remove(uuid);
if (mMenu == null || item == null) {
return;
}
final MenuItem menuItem = mMenu.findItem(item.id);
if (menuItem != null) {
mMenu.removeItem(item.id);
}
}
/**
* Updates the addon menu/webextension browser action with the specified UUID.
*/
private void updateAddonMenuItem(String uuid, final GeckoBundle options) {
// Set attribute for the menu item in cache, if available
final MenuItemInfo item = mAddonMenuItemsCache.get(uuid);
if (item != null) {
item.label = options.getString("name", item.label);
item.checkable = options.getBoolean("checkable", item.checkable);
item.checked = options.getBoolean("checked", item.checked);
item.enabled = options.getBoolean("enabled", item.enabled);
item.visible = options.getBoolean("visible", item.visible);
}
if (mMenu == null || item == null) {
return;
}
final MenuItem menuItem = mMenu.findItem(item.id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
}
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private static void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
final Menu destination;
if (info.parent == 0) {
destination = menu;
} else if (info.parent == GECKO_TOOLS_MENU_ID) {
// The tools menu only exists in our -v11 resources.
final MenuItem tools = menu.findItem(R.id.tools);
destination = tools != null ? tools.getSubMenu() : menu;
} else {
final MenuItem parent = menu.findItem(info.parent);
if (parent == null) {
return;
}
Menu parentMenu = findParentMenu(menu, parent);
if (!parent.hasSubMenu()) {
parentMenu.removeItem(parent.getItemId());
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
if (parent.getIcon() != null) {
((SubMenu) destination).getItem().setIcon(parent.getIcon());
}
} else {
destination = parent.getSubMenu();
}
}
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("item", info.uuid);
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
private static Menu findParentMenu(Menu menu, MenuItem item) {
final int itemId = item.getItemId();
final int count = (menu != null) ? menu.size() : 0;
for (int i = 0; i < count; i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.getItemId() == itemId) {
return menu;
}
if (menuItem.hasSubMenu()) {
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
if (parent != null) {
return parent;
}
}
}
return null;
}
}

View file

@ -34,8 +34,6 @@ import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -56,7 +54,6 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
@ -161,7 +158,6 @@ import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.IntentUtils;
import org.mozilla.gecko.util.MenuUtils;
import org.mozilla.gecko.util.NetworkUtils;
import org.mozilla.gecko.util.PrefUtils;
import org.mozilla.gecko.util.ShortcutUtils;
import org.mozilla.gecko.util.StringUtils;
@ -179,7 +175,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
@ -216,8 +211,6 @@ public class BrowserApp extends GeckoApp
private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";
private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
private static final String STATE_ADDON_MENU_ITEM_CACHE = "menuitems_cache";
private static final String STATE_BROWSER_ACTION_ITEM_CACHE = "browseractions_cache";
private static final String BROWSER_SEARCH_TAG = "browser_search";
@ -261,9 +254,6 @@ public class BrowserApp extends GeckoApp
private ActionModeCompat mActionMode;
private TabHistoryController tabHistoryController;
private static final int GECKO_TOOLS_MENU = -1;
private static final int ADDON_MENU_OFFSET = 1000;
private static final int BROWSER_ACTION_MENU_OFFSET = 10000;
public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
// When the static action bar is shown, only the real toolbar chrome should be
@ -276,98 +266,12 @@ public class BrowserApp extends GeckoApp
private Intent startingIntentAfterPip;
private boolean isInAutomation;
private static class MenuItemInfo implements Parcelable {
public int id;
public String label;
public boolean checkable;
public boolean checked;
public boolean enabled = true;
public boolean visible = true;
public int parent;
public boolean added; // So we can re-add after a locale change.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(label);
dest.writeInt(checkable ? 1 : 0);
dest.writeInt(checked ? 1 : 0);
dest.writeInt(enabled ? 1 : 0);
dest.writeInt(visible ? 1 : 0);
dest.writeInt(parent);
dest.writeInt(added ? 1 : 0);
}
public static final Parcelable.Creator<MenuItemInfo> CREATOR
= new Parcelable.Creator<MenuItemInfo>() {
@Override
public MenuItemInfo createFromParcel(Parcel source) {
return new MenuItemInfo(source);
}
@Override
public MenuItemInfo[] newArray(int size) {
return new MenuItemInfo[size];
}
};
private MenuItemInfo(Parcel source) {
id = source.readInt();
label = source.readString();
checkable = source.readInt() != 0;
checked = source.readInt() != 0;
enabled = source.readInt() != 0;
visible = source.readInt() != 0;
parent = source.readInt();
added = source.readInt() != 0;
}
public MenuItemInfo() { }
}
private static class BrowserActionItemInfo extends MenuItemInfo {
public String uuid;
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(uuid);
}
public static final Parcelable.Creator<BrowserActionItemInfo> CREATOR
= new Parcelable.Creator<BrowserActionItemInfo>() {
@Override
public BrowserActionItemInfo createFromParcel(Parcel source) {
return new BrowserActionItemInfo(source);
}
@Override
public BrowserActionItemInfo[] newArray(int size) {
return new BrowserActionItemInfo[size];
}
};
private BrowserActionItemInfo(Parcel source) {
super(source);
uuid = source.readString();
}
public BrowserActionItemInfo() { }
}
// The types of guest mode dialogs we show.
public static enum GuestModeDialog {
ENTERING,
LEAVING
}
private ArrayList<MenuItemInfo> mAddonMenuItemsCache;
private ArrayList<BrowserActionItemInfo> mBrowserActionItemsCache;
private PropertyAnimator mMainLayoutAnimator;
private static final Interpolator sTabsInterpolator = new Interpolator() {
@ -774,14 +678,6 @@ public class BrowserApp extends GeckoApp
}
});
// If the activity is being restored, the add-ons menu item cache only needs restoring if
// Gecko is already running. Otherwise, we'll simply catch the corresponding events when
// Gecko and the add-ons are starting up.
if (savedInstanceState != null && mIsRestoringActivity) {
mAddonMenuItemsCache = savedInstanceState.getParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE);
mBrowserActionItemsCache = savedInstanceState.getParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE);
}
app.getLightweightTheme().addListener(this);
mProgressView = (AnimatedProgressBar) findViewById(R.id.page_progress);
@ -883,12 +779,6 @@ public class BrowserApp extends GeckoApp
EventDispatcher.getInstance().registerUiThreadListener(this,
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
"Menu:Remove",
"Menu:AddBrowserAction",
"Menu:RemoveBrowserAction",
"Menu:UpdateBrowserAction",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
@ -1580,6 +1470,8 @@ public class BrowserApp extends GeckoApp
final GeckoApplication app = (GeckoApplication) getApplication();
app.getLightweightTheme().removeListener(this);
AddonUICache.getInstance().onDestroyOptionsMenu();
if (mBrowserToolbar != null)
mBrowserToolbar.onDestroy();
@ -1622,12 +1514,6 @@ public class BrowserApp extends GeckoApp
EventDispatcher.getInstance().unregisterUiThreadListener(this,
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
"Menu:Remove",
"Menu:AddBrowserAction",
"Menu:RemoveBrowserAction",
"Menu:UpdateBrowserAction",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
@ -1886,53 +1772,6 @@ public class BrowserApp extends GeckoApp
openOptionsMenu();
break;
case "Menu:Update":
updateAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET,
message.getBundle("options"));
break;
case "Menu:Add":
final MenuItemInfo info = new MenuItemInfo();
info.label = message.getString("name");
if (info.label == null) {
Log.e(LOGTAG, "Invalid menu item name");
return;
}
info.id = message.getInt("id") + ADDON_MENU_OFFSET;
info.checked = message.getBoolean("checked", false);
info.enabled = message.getBoolean("enabled", true);
info.visible = message.getBoolean("visible", true);
info.checkable = message.getBoolean("checkable", false);
final int parent = message.getInt("parent", 0);
info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
addAddonMenuItem(info);
break;
case "Menu:Remove":
removeAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET);
break;
case "Menu:AddBrowserAction":
final BrowserActionItemInfo browserAction = new BrowserActionItemInfo();
browserAction.label = message.getString("name");
if (TextUtils.isEmpty(browserAction.label)) {
Log.e(LOGTAG, "Invalid browser action name");
return;
}
browserAction.id = message.getInt("id") + BROWSER_ACTION_MENU_OFFSET;
browserAction.uuid = message.getString("uuid");
addBrowserActionMenuItem(browserAction);
break;
case "Menu:RemoveBrowserAction":
removeBrowserActionMenuItem(message.getString("uuid"));
break;
case "Menu:UpdateBrowserAction":
updateBrowserActionMenuItem(message.getString("uuid"),
message.getBundle("options"));
break;
case "LightweightTheme:Update":
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
break;
@ -2454,15 +2293,6 @@ public class BrowserApp extends GeckoApp
super.onSaveInstanceState(outState);
mDynamicToolbar.onSaveInstanceState(outState);
outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop());
// The various add-on UI item caches and event listeners should really live somewhere based
// on the Application, so that their lifetime more closely matches that of Gecko itself, as
// GeckoView-based activities can start Gecko (and therefore add-ons) while BrowserApp isn't
// even running.
// For now we'll only guard against the case where BrowserApp is destroyed and later re-
// created while Gecko keeps running throughout, and leave the full solution to bug 1414084.
outState.putParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE, mAddonMenuItemsCache);
outState.putParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE, mBrowserActionItemsCache);
}
/**
@ -3213,244 +3043,6 @@ public class BrowserApp extends GeckoApp
}
}
private static Menu findParentMenu(Menu menu, MenuItem item) {
final int itemId = item.getItemId();
final int count = (menu != null) ? menu.size() : 0;
for (int i = 0; i < count; i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.getItemId() == itemId) {
return menu;
}
if (menuItem.hasSubMenu()) {
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
if (parent != null) {
return parent;
}
}
}
return null;
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
info.added = true;
final Menu destination;
if (info.parent == 0) {
destination = menu;
} else if (info.parent == GECKO_TOOLS_MENU) {
// The tools menu only exists in our -v11 resources.
final MenuItem tools = menu.findItem(R.id.tools);
destination = tools != null ? tools.getSubMenu() : menu;
} else {
final MenuItem parent = menu.findItem(info.parent);
if (parent == null) {
return;
}
Menu parentMenu = findParentMenu(menu, parent);
if (!parent.hasSubMenu()) {
parentMenu.removeItem(parent.getItemId());
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
if (parent.getIcon() != null) {
((SubMenu) destination).getItem().setIcon(parent.getIcon());
}
} else {
destination = parent.getSubMenu();
}
}
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putInt("item", info.id - ADDON_MENU_OFFSET);
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
private void addAddonMenuItem(final MenuItemInfo info) {
if (mAddonMenuItemsCache == null) {
mAddonMenuItemsCache = new ArrayList<MenuItemInfo>();
}
// Mark it as added if the menu was ready.
info.added = (mMenu != null);
// Always cache so we can rebuild after a locale switch.
mAddonMenuItemsCache.add(info);
if (mMenu == null) {
return;
}
addAddonMenuItemToMenu(mMenu, info);
}
private void removeAddonMenuItem(int id) {
// Remove add-on menu item from cache, if available.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.id == id) {
mAddonMenuItemsCache.remove(item);
break;
}
}
}
if (mMenu == null)
return;
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null)
mMenu.removeItem(id);
}
private void updateAddonMenuItem(int id, final GeckoBundle options) {
// Set attribute for the menu item in cache, if available
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.id == id) {
item.label = options.getString("name", item.label);
item.checkable = options.getBoolean("checkable", item.checkable);
item.checked = options.getBoolean("checked", item.checked);
item.enabled = options.getBoolean("enabled", item.enabled);
item.visible = options.getBoolean("visible", item.visible);
item.added = (mMenu != null);
break;
}
}
}
if (mMenu == null) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
}
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private void addBrowserActionMenuItemToMenu(final Menu menu, final BrowserActionItemInfo info) {
info.added = true;
final MenuItem item = menu.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("item", info.uuid);
EventDispatcher.getInstance().dispatch("Menu:BrowserActionClicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
/**
* Adds a WebExtension browser action to the menu.
*/
private void addBrowserActionMenuItem(final BrowserActionItemInfo info) {
if (mBrowserActionItemsCache == null) {
mBrowserActionItemsCache = new ArrayList<BrowserActionItemInfo>();
}
// Mark it as added if the menu was ready.
info.added = (mMenu != null);
// Always cache so we can rebuild after a locale switch.
mBrowserActionItemsCache.add(info);
if (mMenu == null) {
return;
}
addBrowserActionMenuItemToMenu(mMenu, info);
}
/**
* Removes a WebExtension browser action from the menu by its UUID.
*/
private void removeBrowserActionMenuItem(String uuid) {
int id = -1;
// Remove browser action menu item from cache, if available.
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
mBrowserActionItemsCache.remove(item);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
mMenu.removeItem(id);
}
}
/**
* Updates the WebExtension browser action with the specified UUID.
*/
private void updateBrowserActionMenuItem(String uuid, final GeckoBundle options) {
int id = -1;
// Set attribute for the menu item in cache, if available
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
item.label = options.getString("name", item.label);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Sets mMenu = menu.
@ -3465,19 +3057,9 @@ public class BrowserApp extends GeckoApp
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.browser_app_menu, mMenu);
// Add browser action menu items, if any exist.
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
addBrowserActionMenuItemToMenu(mMenu, item);
}
}
// Add add-on menu items, if any exist.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
addAddonMenuItemToMenu(mMenu, item);
}
}
// Let the AddonUICache handle adding (and removing again) any add-on/browser action
// menu items as required.
AddonUICache.getInstance().onCreateOptionsMenu(mMenu);
// Action providers are available only ICS+.
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);

View file

@ -334,6 +334,7 @@ public class GeckoApplication extends Application
FilePicker.init(context);
DownloadsIntegration.init();
HomePanelsManager.getInstance().init(context);
AddonUICache.getInstance().init();
GlobalPageMetadata.getInstance().init();

View file

@ -5,6 +5,7 @@
package org.mozilla.gecko.toolbar;
import org.mozilla.gecko.AddonUICache;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
@ -14,11 +15,7 @@ import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.pwa.PwaUtils;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.ResourceDrawableUtils;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ResourceDrawableUtils;
import org.mozilla.gecko.util.ShortcutUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.GeckoPopupMenu;
@ -40,6 +37,7 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
@ -47,17 +45,21 @@ import java.util.ArrayList;
import static org.mozilla.gecko.toolbar.PageActionLayout.PageAction.UUID_PAGE_ACTION_PWA;
public class PageActionLayout extends ThemedLinearLayout implements BundleEventListener,
View.OnClickListener,
View.OnLongClickListener {
public class PageActionLayout extends ThemedLinearLayout
implements View.OnClickListener, View.OnLongClickListener {
private static final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
private static final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
public static final String PREF_PWA_ONBOARDING = GeckoPreferences.NON_PREF_PREFIX + "pref_pwa_onboarding";
public interface PageActionLayoutDelegate {
void addPageAction(GeckoBundle message);
void removePageAction(GeckoBundle message);
void setCachedPageActions(List<PageAction> cachedPageActions);
}
private final Context mContext;
private final LinearLayout mLayout;
private final List<PageAction> mPageActionList;
private List<PageAction> mPageActionList;
private GeckoPopupMenu mPageActionsMenu;
@ -69,26 +71,51 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
super(context, attrs);
mContext = context;
mLayout = this;
mPageActionList = new ArrayList<PageAction>();
setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
refreshPageActionIcons();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
EventDispatcher.getInstance().registerUiThreadListener(this,
"PageActions:Add",
"PageActions:Remove");
// Calling this will cause the AddonUICache to synchronously call addPageAction for all
// PageAction messages it received while no PageActionLayout was available.
// Processing those messages entails converting a data: URI into a drawable, which can take
// a few ms per message and therefore in theory cause some jank.
// In practice however, PageAction messages are commonly only generated when tabs (from the
// BrowserApp UI) are actually loading, so the AddonUICache's message queueing mechanism
// only becomes relevant if the BrowserApp UI has then been backgrounded *and* subsequently
// destroyed by the OS while some tabs are still loading. Merely starting up Gecko through a
// GeckoView-based activity on the other hand is not enough to queue up PageAction messages.
// Therefore, this case should happen rarely enough that we can tolerate it without having
// to think about moving the image processing into some asynchronous code path.
AddonUICache.getInstance().setPageActionLayoutDelegate(new PageActionLayoutDelegate() {
@Override
public void addPageAction(final GeckoBundle message) {
onAddPageAction(message);
}
@Override
public void removePageAction(final GeckoBundle message) {
onRemovePageAction(message);
}
@Override
public void setCachedPageActions(final List<PageAction> cachedPageActions) {
if (cachedPageActions != null) {
mPageActionList = cachedPageActions;
} else {
mPageActionList = new ArrayList<>();
}
setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
refreshPageActionIcons();
}
});
}
@Override
protected void onDetachedFromWindow() {
EventDispatcher.getInstance().unregisterUiThreadListener(this,
"PageActions:Add",
"PageActions:Remove");
AddonUICache.getInstance().removePageActionLayoutDelegate(mPageActionList);
super.onDetachedFromWindow();
}
@ -99,7 +126,7 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child instanceof ThemedImageButton) {
((ThemedImageButton) child).setPrivateMode(true);
((ThemedImageButton) child).setPrivateMode(isPrivate);
}
}
}
@ -116,52 +143,51 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
}
}
@Override // BundleEventListener
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
private void onAddPageAction(final GeckoBundle message) {
ThreadUtils.assertOnUiThread();
hidePreviousConfirmPrompt();
if ("PageActions:Add".equals(event)) {
final String id = message.getString("id");
final String id = message.getString("id");
boolean alreadyAdded = isPwaAdded(id);
if (alreadyAdded) {
return;
if (isPageActionAlreadyAdded(id)) {
return;
}
maybeShowPwaOnboarding(id);
final String title = message.getString("title");
final String imageURL = message.getString("icon");
final boolean important = message.getBoolean("important");
final boolean useTint = message.getBoolean("useTint");
addPageAction(id, title, imageURL, useTint, new OnPageActionClickListeners() {
@Override
public void onClick(final String id) {
if (UUID_PAGE_ACTION_PWA.equals(id)) {
mPwaConfirm = PwaConfirm.show(getContext());
return;
}
final GeckoBundle data = new GeckoBundle(1);
data.putString("id", id);
EventDispatcher.getInstance().dispatch("PageActions:Clicked", data);
}
maybeShowPwaOnboarding(id);
@Override
public boolean onLongClick(String id) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("id", id);
EventDispatcher.getInstance().dispatch("PageActions:LongClicked", data);
return true;
}
}, important);
}
final String title = message.getString("title");
final String imageURL = message.getString("icon");
final boolean important = message.getBoolean("important");
final boolean useTint = message.getBoolean("useTint");
private void onRemovePageAction(final GeckoBundle message) {
ThreadUtils.assertOnUiThread();
addPageAction(id, title, imageURL, useTint, new OnPageActionClickListeners() {
@Override
public void onClick(final String id) {
if (UUID_PAGE_ACTION_PWA.equals(id)) {
mPwaConfirm = PwaConfirm.show(getContext());
return;
}
final GeckoBundle data = new GeckoBundle(1);
data.putString("id", id);
EventDispatcher.getInstance().dispatch("PageActions:Clicked", data);
}
@Override
public boolean onLongClick(String id) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("id", id);
EventDispatcher.getInstance().dispatch("PageActions:LongClicked", data);
return true;
}
}, important);
} else if ("PageActions:Remove".equals(event)) {
removePageAction(message.getString("id"));
}
hidePreviousConfirmPrompt();
removePageAction(message.getString("id"));
}
private void maybeShowPwaOnboarding(String id) {
@ -180,7 +206,7 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
}
}
private boolean isPwaAdded(String id) {
private boolean isPageActionAlreadyAdded(String id) {
for (PageAction pageAction : mPageActionList) {
if (pageAction.getID() != null && pageAction.getID().equals(id)) {
return true;
@ -223,14 +249,8 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
private void removePageAction(String id) {
ThreadUtils.assertOnUiThread();
final Iterator<PageAction> iter = mPageActionList.iterator();
while (iter.hasNext()) {
final PageAction pageAction = iter.next();
if (pageAction.getID().equals(id)) {
iter.remove();
refreshPageActionIcons();
return;
}
if (PageAction.removeFromList(mPageActionList, id)) {
refreshPageActionIcons();
}
}
@ -453,6 +473,19 @@ public class PageActionLayout extends ThemedLinearLayout implements BundleEventL
return false;
}
/**
* @return True if any PageAction was actually removed, false otherwise.
*/
public static boolean removeFromList(Collection<PageAction> pageActionCollection, String id) {
final Iterator<PageAction> iter = pageActionCollection.iterator();
while (iter.hasNext()) {
final PageAction action = iter.next();
if (action.getID().equals(id)) {
iter.remove();
return true;
}
}
return false;
}
}
}

View file

@ -2336,8 +2336,8 @@ var NativeWindow = {
menu: {
_callbacks: [],
_menuId: 1,
toolsMenuID: -1,
// This value must be kept in sync with GECKO_TOOLS_MENU_UUID in AddonUICache.java.
toolsMenuID: "{115b9308-2023-44f1-a4e9-3e2197669f07}",
add: function() {
let options;
if (arguments.length == 1) {
@ -2353,25 +2353,24 @@ var NativeWindow = {
}
options.type = "Menu:Add";
options.id = this._menuId;
options.uuid = uuidgen.generateUUID().toString();
GlobalEventDispatcher.sendRequest(options);
this._callbacks[this._menuId] = options.callback;
this._menuId++;
return this._menuId - 1;
this._callbacks[options.uuid] = options.callback;
return options.uuid;
},
remove: function(aId) {
GlobalEventDispatcher.sendRequest({ type: "Menu:Remove", id: aId });
remove: function(aUuid) {
GlobalEventDispatcher.sendRequest({ type: "Menu:Remove", uuid: aUuid });
},
update: function(aId, aOptions) {
update: function(aUuid, aOptions) {
if (!aOptions)
return;
GlobalEventDispatcher.sendRequest({
type: "Menu:Update",
id: aId,
uuid: aUuid,
options: aOptions
});
}

View file

@ -11,6 +11,8 @@ import shutil
import subprocess
import zipfile
from zipfile import ZipFile
import mozpack.path as mozpath
from mozfile import TemporaryDirectory
@ -432,7 +434,13 @@ class MachCommands(MachCommandBase):
self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS'] + ["--continue"] + args,
verbose=True)
return ret
if ret != 0:
return ret
# The zip archive is passed along in CI to ship geckoview onto a maven repo
_craft_maven_zip_archive(self.topobjdir)
return 0
@SubCommand('android', 'geckoview-docs',
"""Create GeckoView javadoc and optionally upload to Github""")
@ -580,6 +588,26 @@ class MachCommands(MachCommandBase):
pass
def _get_maven_archive_abs_and_relative_paths(maven_folder):
for subdir, _, files in os.walk(maven_folder):
for file in files:
full_path = os.path.join(subdir, file)
relative_path = os.path.relpath(full_path, maven_folder)
# maven-metadata is intended to be generated on the real maven server
if 'maven-metadata.xml' not in relative_path:
yield full_path, relative_path
def _craft_maven_zip_archive(topobjdir):
geckoview_folder = os.path.join(topobjdir, 'gradle/build/mobile/android/geckoview')
maven_folder = os.path.join(geckoview_folder, 'maven')
with ZipFile(os.path.join(geckoview_folder, 'target.maven.zip'), 'w') as target_zip:
for abs, rel in _get_maven_archive_abs_and_relative_paths(maven_folder):
target_zip.write(abs, arcname=rel)
@CommandProvider
class AndroidEmulatorCommands(MachCommandBase):
"""

View file

@ -14,8 +14,6 @@ var BrowserActions = {
_initialized: false,
_nextMenuId: 0,
/**
* Registers the listeners only if they have not been initialized
* already and there is at least one browser action.
@ -23,7 +21,7 @@ var BrowserActions = {
_maybeRegisterListeners() {
if (!this._initialized && Object.keys(this._browserActions).length) {
this._initialized = true;
EventDispatcher.instance.registerListener(this, "Menu:BrowserActionClicked");
EventDispatcher.instance.registerListener(this, "Menu:Clicked");
}
},
@ -34,26 +32,27 @@ var BrowserActions = {
_maybeUnregisterListeners() {
if (this._initialized && !Object.keys(this._browserActions).length) {
this._initialized = false;
EventDispatcher.instance.unregisterListener(this, "Menu:BrowserActionClicked");
EventDispatcher.instance.unregisterListener(this, "Menu:Clicked");
}
},
/**
* Called when a browser action is clicked on.
* @param {string} event The name of the event, which should always
* be "Menu:BrowserActionClicked".
* be "Menu:Clicked".
* @param {Object} data An object containing information about the
* browser action, which in this case should contain an `item`
* property which is browser action's ID.
* property which is browser action's UUID.
*/
onEvent(event, data) {
if (event !== "Menu:BrowserActionClicked") {
throw new Error(`Expected "Menu:BrowserActionClicked" event - received "${event}" instead`);
if (event !== "Menu:Clicked") {
throw new Error(`Expected "Menu:Clicked" event - received "${event}" instead`);
}
let browserAction = this._browserActions[data.item];
if (!browserAction) {
throw new Error(`No browser action found with id ${data.item}`);
// This was probably meant for the NativeWindow menu handler.
return;
}
browserAction.onClicked();
},
@ -64,8 +63,7 @@ var BrowserActions = {
*/
register(browserAction) {
EventDispatcher.instance.sendRequest({
type: "Menu:AddBrowserAction",
id: this._nextMenuId++,
type: "Menu:Add",
uuid: browserAction.uuid,
name: browserAction.defaults.name,
});
@ -84,7 +82,7 @@ var BrowserActions = {
update(uuid, options) {
if (options.name) {
EventDispatcher.instance.sendRequest({
type: "Menu:UpdateBrowserAction",
type: "Menu:Update",
uuid,
options,
});
@ -125,7 +123,7 @@ var BrowserActions = {
},
/**
* Unregisters the browser action with the specified ID.
* Unregisters the browser action with the specified UUID.
* @param {string} uuid The UUID of the browser action.
*/
unregister(uuid) {
@ -134,7 +132,7 @@ var BrowserActions = {
throw new Error(`No BrowserAction with UUID ${uuid} was found`);
}
EventDispatcher.instance.sendRequest({
type: "Menu:RemoveBrowserAction",
type: "Menu:Remove",
uuid,
});
delete this._browserActions[uuid];

View file

@ -99,8 +99,8 @@ Source code can be obtained by running
hg clone https://hg.mozilla.org/mozilla-unified
Or, if you prefer Git, you should install git-cinnabar, and follow the
instruction here to clone from the Mercurial repository:
Or, if you prefer Git, by following the instruction here to clone from the
Mercurial repository:
https://github.com/glandium/git-cinnabar/wiki/Mozilla:-A-git-workflow-for-Gecko-development
@ -121,17 +121,26 @@ optimally configured?
Please enter your reply: '''
CLONE_MERCURIAL = '''
If you would like to clone the mozilla-unified Mercurial repository, please
enter the destination path below.
CONFIGURE_GIT = '''
Mozilla recommends using git-cinnabar to work with mozilla-central.
(If you prefer to use Git, leave this blank.)
Would you like to run a few configuration steps to ensure Git is
optimally configured?
1. Yes
2. No
Please enter your reply: '''
CLONE_VCS = '''
If you would like to clone the {} {} repository, please
enter the destination path below.
'''
CLONE_MERCURIAL_PROMPT = '''
Destination directory for Mercurial clone (leave empty to not clone): '''.lstrip()
CLONE_VCS_PROMPT = '''
Destination directory for {} clone (leave empty to not clone): '''.lstrip()
CLONE_MERCURIAL_NOT_EMPTY = '''
CLONE_VCS_NOT_EMPTY = '''
ERROR! Destination directory '{}' is not empty.
Would you like to clone to '{}'?
@ -142,11 +151,11 @@ Would you like to clone to '{}'?
Please enter your reply: '''.lstrip()
CLONE_MERCURIAL_NOT_EMPTY_FALLBACK_FAILED = '''
CLONE_VCS_NOT_EMPTY_FALLBACK_FAILED = '''
ERROR! Destination directory '{}' is not empty.
'''
CLONE_MERCURIAL_NOT_DIR = '''
CLONE_VCS_NOT_DIR = '''
ERROR! Destination '{}' exists but is not a directory.
'''
@ -166,6 +175,16 @@ DEBIAN_DISTROS = (
'"elementary"'
)
ADD_GIT_TOOLS_PATH = '''
To add git-cinnabar to the PATH, edit your shell initialization script, which
may be called ~/.bashrc or ~/.bash_profile or ~/.profile, and add the following
lines:
export PATH="{}:$PATH"
Then restart your shell.
'''
class Bootstrapper(object):
"""Main class that performs system bootstrap."""
@ -231,11 +250,16 @@ class Bootstrapper(object):
self.instance = cls(**args)
def input_clone_dest(self):
print(CLONE_MERCURIAL)
def input_clone_dest(self, with_hg=True):
repo_name = 'mozilla-unified'
vcs = 'Mercurial'
if not with_hg:
repo_name = 'gecko'
vcs = 'Git'
print(CLONE_VCS.format(repo_name, vcs))
while True:
dest = raw_input(CLONE_MERCURIAL_PROMPT)
dest = raw_input(CLONE_VCS_PROMPT.format(vcs))
dest = dest.strip()
if not dest:
return ''
@ -245,18 +269,18 @@ class Bootstrapper(object):
return dest
if not os.path.isdir(dest):
print(CLONE_MERCURIAL_NOT_DIR.format(dest))
print(CLONE_VCS_NOT_DIR.format(dest))
continue
if os.listdir(dest) == []:
return dest
newdest = os.path.join(dest, 'mozilla-unified')
newdest = os.path.join(dest, repo_name)
if os.path.exists(newdest):
print(CLONE_MERCURIAL_NOT_EMPTY_FALLBACK_FAILED.format(dest))
print(CLONE_VCS_NOT_EMPTY_FALLBACK_FAILED.format(dest))
continue
choice = self.instance.prompt_int(prompt=CLONE_MERCURIAL_NOT_EMPTY.format(dest,
choice = self.instance.prompt_int(prompt=CLONE_VCS_NOT_EMPTY.format(dest,
newdest), low=1, high=3)
if choice == 1:
return newdest
@ -360,7 +384,6 @@ class Bootstrapper(object):
(checkout_type, checkout_root) = r
# Possibly configure Mercurial, but not if the current checkout is Git.
# TODO offer to configure Git.
if hg_installed and state_dir_available and checkout_type != 'git':
configure_hg = False
if not self.instance.no_interactive:
@ -374,6 +397,21 @@ class Bootstrapper(object):
if configure_hg:
configure_mercurial(self.instance.which('hg'), state_dir)
# Offer to configure Git, if the current checkout is Git.
elif self.instance.which('git') and checkout_type == 'git':
should_configure_git = False
if not self.instance.no_interactive:
choice = self.instance.prompt_int(prompt=CONFIGURE_GIT,
low=1, high=2)
if choice == 1:
should_configure_git = True
else:
# Assuming default configuration setting applies to all VCS.
should_configure_git = self.hg_configure
if should_configure_git:
configure_git(self.instance.which('git'), state_dir)
# Offer to clone if we're not inside a clone.
have_clone = False
@ -382,7 +420,14 @@ class Bootstrapper(object):
elif hg_installed and not self.instance.no_interactive:
dest = self.input_clone_dest()
if dest:
have_clone = clone_firefox(self.instance.which('hg'), dest)
have_clone = hg_clone_firefox(self.instance.which('hg'), dest)
checkout_root = dest
elif self.instance.which('git') and checkout_type == 'git':
dest = self.input_clone_dest(False)
if dest:
git = self.instance.which('git')
watchman = self.instance.which('watchman')
have_clone = git_clone_firefox(git, dest, watchman)
checkout_root = dest
if not have_clone:
@ -470,7 +515,7 @@ def update_mercurial_repo(hg, url, dest, revision):
print('=' * 80)
def clone_firefox(hg, dest):
def hg_clone_firefox(hg, dest):
"""Clone the Firefox repository to a specified destination."""
print('Cloning Firefox Mercurial repository to %s' % dest)
@ -562,3 +607,90 @@ def current_firefox_checkout(check_output, env, hg=None):
break
return (None, None)
def update_git_tools(git, root_state_dir):
"""Ensure git-cinnabar is up to date."""
cinnabar_dir = os.path.join(root_state_dir, 'git-cinnabar')
# Ensure the latest revision of git-cinnabar is present.
update_git_repo(git, 'https://github.com/glandium/git-cinnabar.git',
cinnabar_dir)
# Perform a download of cinnabar.
download_args = [git, 'cinnabar', 'download']
try:
subprocess.check_call(download_args, cwd=cinnabar_dir)
except subprocess.CalledProcessError as e:
print(e)
return cinnabar_dir
def update_git_repo(git, url, dest):
"""Perform a clone/pull + update of a Git repository."""
pull_args = [git]
if os.path.exists(dest):
pull_args.extend(['pull'])
cwd = dest
else:
pull_args.extend(['clone', '--no-checkout', url, dest])
cwd = '/'
update_args = [git, 'checkout']
print('=' * 80)
print('Ensuring %s is up to date at %s' % (url, dest))
try:
subprocess.check_call(pull_args, cwd=cwd)
subprocess.check_call(update_args, cwd=dest)
finally:
print('=' * 80)
def configure_git(git, root_state_dir):
"""Run the Git configuration steps."""
cinnabar_dir = update_git_tools(git, root_state_dir)
print(ADD_GIT_TOOLS_PATH.format(cinnabar_dir))
def git_clone_firefox(git, dest, watchman=None):
"""Clone the Firefox repository to a specified destination."""
print('Cloning Firefox repository to %s' % dest)
try:
# Configure git per the git-cinnabar requirements.
subprocess.check_call([git, 'clone', '-b', 'bookmarks/central',
'hg::https://hg.mozilla.org/mozilla-unified', dest])
subprocess.check_call([git, 'remote', 'add', 'inbound',
'hg::ssh://hg.mozilla.org/integration/mozilla-inbound'],
cwd=dest)
subprocess.check_call([git, 'config', 'remote.inbound.skipDefaultUpdate',
'true'], cwd=dest)
subprocess.check_call([git, 'config', 'remote.inbound.push',
'+HEAD:refs/heads/branches/default/tip'], cwd=dest)
subprocess.check_call([git, 'config', 'fetch.prune', 'true'], cwd=dest)
subprocess.check_call([git, 'config', 'pull.ff', 'only'], cwd=dest)
watchman_sample = os.path.join(dest, '.git/hooks/fsmonitor-watchman.sample')
# Older versions of git didn't include fsmonitor-watchman.sample.
if watchman and watchman_sample:
print('Configuring watchman')
watchman_config = os.path.join(dest, '.git/hooks/query-watchman')
if not os.path.exists(watchman_config):
print('Copying %s to %s' % (watchman_sample, watchman_config))
copy_args = ['cp', '.git/hooks/fsmonitor-watchman.sample',
'.git/hooks/query-watchman']
subprocess.check_call(copy_args, cwd=dest)
config_args = [git, 'config', 'core.fsmonitor', '.git/hooks/query-watchman']
subprocess.check_call(config_args, cwd=dest)
except Exception as e:
print(e)
return False
print('Firefox source code available at %s' % dest)
return True

View file

@ -43,37 +43,53 @@ class VersionControlCommands(object):
def __init__(self, context):
self._context = context
@Command('mercurial-setup', category='devenv',
description='Help configure Mercurial for optimal development.')
@Command('vcs-setup', category='devenv',
description='Help configure a VCS for optimal development.')
@CommandArgument('-u', '--update-only', action='store_true',
help='Only update recommended extensions, don\'t run the wizard.')
def mercurial_setup(self, update_only=False):
"""Ensure Mercurial is optimally configured.
@CommandArgument('-g', '--git', action='store_true',
help='Use Git instead of Mercurial.')
def vcs_setup(self, update_only=False, git=False):
"""Ensure a Version Control System (Mercurial or Git) is optimally
configured.
This command will inspect your Mercurial configuration and
This command will inspect your VCS configuration and
guide you through an interactive wizard helping you configure
Mercurial for optimal use on Mozilla projects.
VCS for optimal use on Mozilla projects.
User choice is respected: no changes are made without explicit
confirmation from you.
If "--update-only" is used, the interactive wizard is disabled
and this command only ensures that remote repositories providing
Mercurial extensions are up to date.
VCS extensions are up to date.
If "--git" is used, then Git is selected as the VCS instead of Mercurial,
which is the default.
"""
import which
import mozboot.bootstrap as bootstrap
vcs = 'hg'
if git:
vcs = 'git'
# "hg" is an executable script with a shebang, which will be found
# be which.which. We need to pass a win32 executable to the function
# by which.which. We need to pass a win32 executable to the function
# because we spawn a process
# from it.
if sys.platform in ('win32', 'msys'):
hg = which.which('hg.exe')
vcs = which.which(vcs + '.exe')
else:
hg = which.which('hg')
vcs = which.which(vcs)
if update_only:
bootstrap.update_vct(hg, self._context.state_dir)
if git:
bootstrap.update_git_tools(vcs, self._context.state_dir)
else:
bootstrap.update_vct(vcs, self._context.state_dir)
else:
bootstrap.configure_mercurial(hg, self._context.state_dir)
if git:
bootstrap.configure_git(vcs, self._context.state_dir)
else:
bootstrap.configure_mercurial(vcs, self._context.state_dir)

View file

@ -1174,4 +1174,4 @@ static const TransportSecurityPreload kPublicKeyPinningPreloadList[] = {
static const int32_t kUnknownId = -1;
static const PRTime kPreloadPKPinsExpirationTime = INT64_C(1542276268102000);
static const PRTime kPreloadPKPinsExpirationTime = INT64_C(1542881093590000);

View file

@ -8,7 +8,7 @@
/*****************************************************************************/
#include <stdint.h>
const PRTime gPreloadListExpirationTime = INT64_C(1544695448460000);
const PRTime gPreloadListExpirationTime = INT64_C(1545300209985000);
%%
0-1.party, 1
0.me.uk, 1

File diff suppressed because one or more lines are too long

View file

@ -208,11 +208,12 @@ async function loadDumpFile(bucket, collection) {
class RemoteSettingsClient {
constructor(collectionName, { bucketName, signerName, filterFunc = jexlFilterFunc, lastCheckTimePref }) {
constructor(collectionName, { bucketName, signerName, filterFunc = jexlFilterFunc, localFields = [], lastCheckTimePref }) {
this.collectionName = collectionName;
this.bucketName = bucketName;
this.signerName = signerName;
this.filterFunc = filterFunc;
this.localFields = localFields;
this._lastCheckTimePref = lastCheckTimePref;
this._listeners = new Map();
@ -276,17 +277,26 @@ class RemoteSettingsClient {
/**
* Open the underlying Kinto collection, using the appropriate adapter and
* options. This acts as a context manager where the connection is closed
* once the specified `callback` has finished.
*
* @param {callback} function the async function to execute with the open SQlite connection.
* @param {Object} options additional advanced options.
* @param {string} options.hooks hooks to execute on synchronization (see Kinto.js docs)
* options.
*/
async openCollection(options = {}) {
async openCollection() {
if (!this._kinto) {
this._kinto = new Kinto({ bucket: this.bucketName, adapter: Kinto.adapters.IDB });
}
const options = {
localFields: this.localFields,
};
// If there is a `signerName` and collection signing is enforced, add a
// hook for incoming changes that validates the signature.
const verifySignature = Services.prefs.getBoolPref(PREF_SETTINGS_VERIFY_SIGNATURE, true);
if (this.signerName && verifySignature) {
const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
options.hooks = {
"incoming-changes": [(payload, collection) => {
return this._validateCollectionSignature(remote, payload, collection);
}]
};
}
return this._kinto.collection(this.collectionName, options);
}
@ -335,22 +345,10 @@ class RemoteSettingsClient {
async maybeSync(lastModified, serverTime, options = { loadDump: true }) {
const {loadDump} = options;
const remote = Services.prefs.getCharPref(PREF_SETTINGS_SERVER);
const verifySignature = Services.prefs.getBoolPref(PREF_SETTINGS_VERIFY_SIGNATURE, true);
// if there is a signerName and collection signing is enforced, add a
// hook for incoming changes that validates the signature
const colOptions = {};
if (this.signerName && verifySignature) {
colOptions.hooks = {
"incoming-changes": [(payload, collection) => {
return this._validateCollectionSignature(remote, payload, collection);
}]
};
}
let reportStatus = null;
try {
const collection = await this.openCollection(colOptions);
const collection = await this.openCollection();
// Synchronize remote data into a local Sqlite DB.
let collectionLastModified = await collection.db.getLastModified();

View file

@ -85,6 +85,17 @@ add_task(async function test_records_obtained_from_server_are_stored_in_db() {
});
add_task(clear_state);
add_task(async function test_records_can_have_local_fields() {
const c = RemoteSettings("password-fields", { localFields: ["accepted"] });
await c.maybeSync(2000, Date.now());
const col = await c.openCollection();
await col.update({ id: "9d500963-d80e-3a91-6e74-66f3811b99cc", accepted: true });
await c.maybeSync(2000, Date.now()); // Does not fail.
});
add_task(clear_state);
add_task(async function test_current_server_time_is_saved_in_pref() {
const serverTime = Date.now();
await client.maybeSync(2000, serverTime);

View file

@ -51,6 +51,7 @@ fn device_size(device: &Device) -> Size2D<Au> {
Size2D::new(Au(width), Au(height))
}
/// https://drafts.csswg.org/mediaqueries-4/#width
fn eval_width(
device: &Device,
value: Option<CSSPixelLength>,
@ -63,6 +64,7 @@ fn eval_width(
)
}
/// https://drafts.csswg.org/mediaqueries-4/#device-width
fn eval_device_width(
device: &Device,
value: Option<CSSPixelLength>,
@ -75,6 +77,7 @@ fn eval_device_width(
)
}
/// https://drafts.csswg.org/mediaqueries-4/#height
fn eval_height(
device: &Device,
value: Option<CSSPixelLength>,
@ -87,6 +90,7 @@ fn eval_height(
)
}
/// https://drafts.csswg.org/mediaqueries-4/#device-height
fn eval_device_height(
device: &Device,
value: Option<CSSPixelLength>,
@ -121,6 +125,7 @@ where
)
}
/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio
fn eval_aspect_ratio(
device: &Device,
query_value: Option<AspectRatio>,
@ -129,6 +134,7 @@ fn eval_aspect_ratio(
eval_aspect_ratio_for(device, query_value, range_or_operator, viewport_size)
}
/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio
fn eval_device_aspect_ratio(
device: &Device,
query_value: Option<AspectRatio>,
@ -137,6 +143,11 @@ fn eval_device_aspect_ratio(
eval_aspect_ratio_for(device, query_value, range_or_operator, device_size)
}
/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio
///
/// FIXME(emilio): This should be an alias of `resolution`, according to the
/// spec, and also according to the code in Chromium. Unify with
/// `eval_resolution`.
fn eval_device_pixel_ratio(
device: &Device,
query_value: Option<f32>,
@ -183,6 +194,7 @@ where
}
}
/// https://drafts.csswg.org/mediaqueries-4/#orientation
fn eval_orientation(
device: &Device,
value: Option<Orientation>,
@ -190,6 +202,7 @@ fn eval_orientation(
eval_orientation_for(device, value, viewport_size)
}
/// FIXME: There's no spec for `-moz-device-orientation`.
fn eval_device_orientation(
device: &Device,
value: Option<Orientation>,
@ -208,6 +221,7 @@ pub enum DisplayMode {
Fullscreen,
}
/// https://w3c.github.io/manifest/#the-display-mode-media-feature
fn eval_display_mode(
device: &Device,
query_value: Option<DisplayMode>,
@ -225,6 +239,7 @@ fn eval_display_mode(
gecko_display_mode as u8 == query_value as u8
}
/// https://drafts.csswg.org/mediaqueries-4/#grid
fn eval_grid(_: &Device, query_value: Option<bool>, _: Option<RangeOrOperator>) -> bool {
// Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature
// is always 0.
@ -232,6 +247,7 @@ fn eval_grid(_: &Device, query_value: Option<bool>, _: Option<RangeOrOperator>)
query_value.map_or(supports_grid, |v| v == supports_grid)
}
/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d
fn eval_transform_3d(
_: &Device,
query_value: Option<bool>,
@ -248,12 +264,14 @@ enum Scan {
Interlace,
}
/// https://drafts.csswg.org/mediaqueries-4/#scan
fn eval_scan(_: &Device, _: Option<Scan>) -> bool {
// Since Gecko doesn't support the 'tv' media type, the 'scan' feature never
// matches.
false
}
/// https://drafts.csswg.org/mediaqueries-4/#color
fn eval_color(
device: &Device,
query_value: Option<u32>,
@ -268,13 +286,13 @@ fn eval_color(
)
}
/// https://drafts.csswg.org/mediaqueries-4/#color-index
fn eval_color_index(
_: &Device,
query_value: Option<u32>,
range_or_operator: Option<RangeOrOperator>,
) -> bool {
// We should return zero if the device does not use a color lookup
// table.
// We should return zero if the device does not use a color lookup table.
let index = 0;
RangeOrOperator::evaluate(
range_or_operator,
@ -283,6 +301,7 @@ fn eval_color_index(
)
}
/// https://drafts.csswg.org/mediaqueries-4/#monochrome
fn eval_monochrome(
_: &Device,
query_value: Option<u32>,
@ -298,6 +317,7 @@ fn eval_monochrome(
)
}
/// https://drafts.csswg.org/mediaqueries-4/#resolution
fn eval_resolution(
device: &Device,
query_value: Option<Resolution>,
@ -319,6 +339,7 @@ enum PrefersReducedMotion {
Reduce,
}
/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion
fn eval_prefers_reduced_motion(
device: &Device,
query_value: Option<PrefersReducedMotion>,

View file

@ -421,46 +421,34 @@ impl NoCalcLength {
value: CSSFloat,
unit: &str,
) -> Result<Self, ()> {
match_ignore_ascii_case! { unit,
"px" => Ok(NoCalcLength::Absolute(AbsoluteLength::Px(value))),
"in" => Ok(NoCalcLength::Absolute(AbsoluteLength::In(value))),
"cm" => Ok(NoCalcLength::Absolute(AbsoluteLength::Cm(value))),
"mm" => Ok(NoCalcLength::Absolute(AbsoluteLength::Mm(value))),
"q" => Ok(NoCalcLength::Absolute(AbsoluteLength::Q(value))),
"pt" => Ok(NoCalcLength::Absolute(AbsoluteLength::Pt(value))),
"pc" => Ok(NoCalcLength::Absolute(AbsoluteLength::Pc(value))),
Ok(match_ignore_ascii_case! { unit,
"px" => NoCalcLength::Absolute(AbsoluteLength::Px(value)),
"in" => NoCalcLength::Absolute(AbsoluteLength::In(value)),
"cm" => NoCalcLength::Absolute(AbsoluteLength::Cm(value)),
"mm" => NoCalcLength::Absolute(AbsoluteLength::Mm(value)),
"q" => NoCalcLength::Absolute(AbsoluteLength::Q(value)),
"pt" => NoCalcLength::Absolute(AbsoluteLength::Pt(value)),
"pc" => NoCalcLength::Absolute(AbsoluteLength::Pc(value)),
// font-relative
"em" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Em(value))),
"ex" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Ex(value))),
"ch" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Ch(value))),
"rem" => Ok(NoCalcLength::FontRelative(FontRelativeLength::Rem(value))),
"em" => NoCalcLength::FontRelative(FontRelativeLength::Em(value)),
"ex" => NoCalcLength::FontRelative(FontRelativeLength::Ex(value)),
"ch" => NoCalcLength::FontRelative(FontRelativeLength::Ch(value)),
"rem" => NoCalcLength::FontRelative(FontRelativeLength::Rem(value)),
// viewport percentages
"vw" => {
if context.in_page_rule() {
return Err(())
}
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(value)))
},
"vh" => {
if context.in_page_rule() {
return Err(())
}
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(value)))
},
"vmin" => {
if context.in_page_rule() {
return Err(())
}
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmin(value)))
},
"vmax" => {
if context.in_page_rule() {
return Err(())
}
Ok(NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmax(value)))
},
_ => Err(())
}
"vw" if !context.in_page_rule() => {
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(value))
}
"vh" if !context.in_page_rule() => {
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(value))
}
"vmin" if !context.in_page_rule() => {
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmin(value))
}
"vmax" if !context.in_page_rule() => {
NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmax(value))
}
_ => return Err(())
})
}
#[inline]

View file

@ -0,0 +1,41 @@
# 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/.
loader: taskgraph.loader.single_dep:loader
transforms:
- taskgraph.transforms.name_sanity:transforms
- taskgraph.transforms.beetmover_geckoview:transforms
- taskgraph.transforms.task:transforms
kind-dependencies:
- build # geckoview builds aren't signed
only-for-attributes:
- nightly
not-for-build-platforms:
- linux-nightly/opt
- linux64-nightly/opt
- macosx64-nightly/opt
- win32-nightly/opt
- win64-nightly/opt
- linux-devedition-nightly/opt
- linux64-devedition-nightly/opt
- macosx64-devedition-nightly/opt
- win32-devedition-nightly/opt
- win64-devedition-nightly/opt
- linux64-asan-reporter-nightly/opt
- win64-asan-reporter-nightly/opt
job-template:
# Beetmoving geckoview makes it available to the official maven repo. So we want beetmover to
# act only when the release is greenlit.
shipping-phase: ship
bucket-scope:
by-project:
mozilla-central: 'project:releng:beetmover:bucket:maven-production'
mozilla-beta: 'project:releng:beetmover:bucket:maven-production'
mozilla-release: 'project:releng:beetmover:bucket:maven-production'
default: 'project:releng:beetmover:bucket:maven-staging'

View file

@ -19,9 +19,13 @@ android-api-16/debug:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
@ -130,9 +134,13 @@ android-x86/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
@ -187,9 +195,13 @@ android-x86-nightly/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
@ -241,9 +253,13 @@ android-api-16/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
@ -293,9 +309,13 @@ android-api-16-without-google-play-services/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview_example.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
type: file
@ -348,9 +368,13 @@ android-api-16-nightly/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
@ -402,9 +426,13 @@ android-aarch64/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
@ -459,9 +487,13 @@ android-aarch64-nightly/opt:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
# TODO Bug 1433198. Remove the following entry once target.maven.zip is uploaded to a maven repository
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/target.maven.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/target.maven.zip
type: file
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file

View file

@ -188,9 +188,11 @@ scriptworker:
- 'project:releng:beetmover:bucket:release'
- 'project:releng:beetmover:bucket:nightly'
- 'project:releng:beetmover:bucket:partner'
- 'project:releng:beetmover:bucket:maven-production'
'scriptworker-prov-v1/beetmoverworker-dev':
- 'project:releng:beetmover:bucket:dep'
- 'project:releng:beetmover:bucket:dep-partner'
- 'project:releng:beetmover:bucket:maven-staging'
'scriptworker-prov-v1/balrogworker-v1':
- 'project:releng:balrog:server:nightly'
- 'project:releng:balrog:server:aurora'

View file

@ -25,7 +25,10 @@ job-defaults:
command: >
cd $GECKO_PATH &&
./mach jsshell-bench --perfherder={shell} --binary=$JSSHELL {test}
run-on-projects: ['mozilla-central', 'try']
run-on-projects:
by-shell:
sm: ['mozilla-central', 'try', 'integration']
default: ['mozilla-central', 'try']
fetches:
build:
- target.jsshell.zip

View file

@ -203,6 +203,11 @@ beetmover-source
Beetmover-source publishes release source. This is part of release promotion.
beetmover-geckoview
-------------------
Beetmover-geckoview publishes the Android library called "geckoview".
checksums-signing
-----------------
Checksums-signing take as input the checksums file generated by beetmover tasks

View file

@ -0,0 +1,156 @@
# 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/.
"""
Transform the beetmover task into an actual task description.
"""
from __future__ import absolute_import, print_function, unicode_literals
from taskgraph.transforms.base import TransformSequence
from taskgraph.transforms.beetmover import \
craft_release_properties as beetmover_craft_release_properties
from taskgraph.util.attributes import copy_attributes_from_dependent_job
from taskgraph.util.schema import validate_schema, Schema, resolve_keyed_by, optionally_keyed_by
from taskgraph.util.scriptworker import (get_phase,
get_worker_type_for_scope)
from taskgraph.transforms.task import task_description_schema
from voluptuous import Required, Optional
_ARTIFACT_ID_PER_PLATFORM = {
'android-aarch64': 'geckoview{update_channel}-arm64-v8a',
'android-api-16': 'geckoview{update_channel}-armeabi-v7a',
'android-x86': 'geckoview{update_channel}-x86',
}
_MOZ_UPDATE_CHANNEL_PER_BRANCH = {
'mozilla-release': '',
'mozilla-beta': '-beta',
'mozilla-central': '-nightly',
'maple': '-nightly-maple',
}
task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
transforms = TransformSequence()
beetmover_description_schema = Schema({
Required('dependent-task'): object,
Required('depname', default='build'): basestring,
Optional('label'): basestring,
Optional('treeherder'): task_description_schema['treeherder'],
Optional('bucket-scope'): optionally_keyed_by('project', basestring),
Optional('shipping-phase'): task_description_schema['shipping-phase'],
Optional('shipping-product'): task_description_schema['shipping-product'],
})
@transforms.add
def validate(config, jobs):
for job in jobs:
label = job.get('dependent-task', object).__dict__.get('label', '?no-label?')
validate_schema(
beetmover_description_schema, job,
"In beetmover-geckoview ({!r} kind) task for {!r}:".format(config.kind, label))
yield job
@transforms.add
def make_task_description(config, jobs):
for job in jobs:
dep_job = job['dependent-task']
attributes = dep_job.attributes
treeherder = job.get('treeherder', {})
treeherder.setdefault('symbol', 'BM-gv')
dep_th_platform = dep_job.task.get('extra', {}).get(
'treeherder', {}).get('machine', {}).get('platform', '')
treeherder.setdefault('platform',
'{}/opt'.format(dep_th_platform))
treeherder.setdefault('tier', 3)
treeherder.setdefault('kind', 'build')
label = job['label']
description = (
"Beetmover submission for geckoview"
"{build_platform}/{build_type}'".format(
build_platform=attributes.get('build_platform'),
build_type=attributes.get('build_type')
)
)
dependent_kind = str(dep_job.kind)
dependencies = {dependent_kind: dep_job.label}
attributes = copy_attributes_from_dependent_job(dep_job)
if job.get('locale'):
attributes['locale'] = job['locale']
resolve_keyed_by(
job, 'bucket-scope', item_name=job['label'],
project=config.params['project']
)
task = {
'label': label,
'description': description,
'worker-type': get_worker_type_for_scope(config, job['bucket-scope']),
'scopes': [job['bucket-scope'], 'project:releng:beetmover:action:push-to-maven'],
'dependencies': dependencies,
'attributes': attributes,
'run-on-projects': ['mozilla-central'],
'treeherder': treeherder,
'shipping-phase': job.get('shipping-phase', get_phase(config)),
}
yield task
def generate_upstream_artifacts(build_task_ref):
return [{
'taskId': {'task-reference': build_task_ref},
'taskType': 'build',
'paths': ['public/build/target.maven.zip'],
'zipExtract': True,
}]
@transforms.add
def make_task_worker(config, jobs):
for job in jobs:
valid_beetmover_job = len(job['dependencies']) == 1 and 'build' in job['dependencies']
if not valid_beetmover_job:
raise NotImplementedError(
'Beetmover-geckoview must have a single dependency. Got: {}'.format(
job['dependencies']
)
)
build_task = list(job["dependencies"].keys())[0]
build_task_ref = "<" + str(build_task) + ">"
worker = {
'implementation': 'beetmover-maven',
'release-properties': craft_release_properties(config, job),
'upstream-artifacts': generate_upstream_artifacts(build_task_ref)
}
job["worker"] = worker
yield job
def craft_release_properties(config, job):
props = beetmover_craft_release_properties(config, job)
platform = props['platform']
update_channel = _MOZ_UPDATE_CHANNEL_PER_BRANCH.get(
props['branch'], '-UNKNOWN_MOZ_UPDATE_CHANNEL'
)
artifact_id = _ARTIFACT_ID_PER_PLATFORM[platform].format(update_channel=update_channel)
props['artifact-id'] = artifact_id
props['app-name'] = 'geckoview' # this beetmover job is not about pushing Fennec
return props

View file

@ -510,6 +510,26 @@ task_description_schema = Schema({
# the maximum time to run, in seconds
Required('max-run-time'): int,
Required('product'): basestring,
}, {
Required('implementation'): 'beetmover-maven',
Required('max-run-time', default=600): int,
Required('release-properties'): {
'app-name': basestring,
'app-version': basestring,
'branch': basestring,
'build-id': basestring,
'artifact-id': basestring,
'hash-type': basestring,
'platform': basestring,
},
Required('upstream-artifacts'): [{
Required('taskId'): taskref_or_string,
Required('taskType'): basestring,
Required('paths'): [basestring],
Required('zipExtract', default=False): bool,
}],
}, {
Required('implementation'): 'balrog',
Required('balrog-action'): Any(*BALROG_ACTIONS),
@ -1077,6 +1097,16 @@ def build_beetmover_push_to_release_payload(config, task, task_def):
}
@payload_builder('beetmover-maven')
def build_beetmover_maven_payload(config, task, task_def):
build_beetmover_payload(config, task, task_def)
task_def['payload']['artifact_id'] = task['worker']['release-properties']['artifact-id']
del task_def['payload']['releaseProperties']['hashType']
del task_def['payload']['releaseProperties']['platform']
@payload_builder('balrog')
def build_balrog_payload(config, task, task_def):
worker = task['worker']
@ -1313,11 +1343,9 @@ def set_defaults(config, tasks):
worker.setdefault('env', {})
worker.setdefault('os-groups', [])
worker.setdefault('chain-of-trust', False)
elif worker['implementation'] == 'scriptworker-signing':
worker.setdefault('max-run-time', 600)
elif worker['implementation'] == 'beetmover':
worker.setdefault('max-run-time', 600)
elif worker['implementation'] == 'beetmover-push-to-release':
elif worker['implementation'] in (
'scriptworker-signing', 'beetmover', 'beetmover-push-to-release', 'beetmover-maven',
):
worker.setdefault('max-run-time', 600)
elif worker['implementation'] == 'push-apk':
worker.setdefault('commit', False)

View file

@ -96,7 +96,7 @@ Getting the code, running tests
just calls code in runtests.py).
* Configure Mercurial with helpful extensions for Mozilla
development by running `./mach mercurial-setup`.
development by running `./mach vcs-setup`.
* It should install extensions like firefox-trees and set
you up to be able to use MozReview, our code-review tool.

View file

@ -51,9 +51,6 @@ class Repackage(BaseScript):
return self.abs_dirs
abs_dirs = super(Repackage, self).query_abs_dirs()
config = self.config
for directory in abs_dirs:
value = abs_dirs[directory]
abs_dirs[directory] = value
dirs = {}
dirs['abs_tools_dir'] = os.path.join(abs_dirs['abs_work_dir'], 'tools')

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