--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1003,17 +1003,17 @@ pref("security.sandbox.content.level", 1
// to whitelist more system calls.
//
// So the purpose of this setting is to allow nightly users to disable the
// sandbox while we fix their problems. This way, they won't have to wait for
// another nightly release which disables seccomp-bpf again.
//
// This setting may not be required anymore once we decide to permanently
// enable the content sandbox.
-pref("security.sandbox.content.level", 1);
+pref("security.sandbox.content.level", 2);
#endif
#if defined(XP_MACOSX) || defined(XP_WIN)
#if defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
// ID (a UUID when set by gecko) that is used to form the name of a
// sandbox-writable temporary directory to be used by content processes
// when a temporary writable file is required in a level 1 sandbox.
pref("security.sandbox.content.tempDirSuffix", "");
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -424,16 +424,17 @@ class BasePopup {
global.PanelPopup = class PanelPopup extends BasePopup {
constructor(extension, imageNode, popupURL, browserStyle) {
let document = imageNode.ownerDocument;
let panel = document.createElement("panel");
panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
panel.setAttribute("class", "browser-extension-panel");
+ panel.setAttribute("tabspecific", "true");
panel.setAttribute("type", "arrow");
panel.setAttribute("role", "group");
document.getElementById("mainPopupSet").appendChild(panel);
super(extension, panel, popupURL, browserStyle);
this.ignoreResizes = false;
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -1,13 +1,15 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testPageActionPopup() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head></html>`;
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"background": {
"page": "data/background.html",
},
"page_action": {
@@ -64,16 +66,22 @@ add_task(function* testPageActionPopup()
},
() => {
browser.pageAction.setPopup({tabId, popup: "/popup-a.html"});
sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
},
() => {
browser.test.sendMessage("next-test", {expectClosed: true});
},
+ () => {
+ sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
+ },
+ () => {
+ browser.test.sendMessage("next-test", {closeOnTabSwitch: true});
+ },
];
let expect = {};
sendClick = ({expectEvent, expectPopup, runNextTest}) => {
expect = {event: expectEvent, popup: expectPopup, runNextTest};
browser.test.sendMessage("send-click");
};
@@ -148,16 +156,30 @@ add_task(function* testPageActionPopup()
if (expecting.expectClosed) {
ok(panel, "Expect panel to exist");
yield promisePopupShown(panel);
extension.sendMessage("close-popup");
yield promisePopupHidden(panel);
ok(true, `Panel is closed`);
+ } else if (expecting.closeOnTabSwitch) {
+ ok(panel, "Expect panel to exist");
+ yield promisePopupShown(panel);
+
+ let oldTab = gBrowser.selectedTab;
+ ok(oldTab != gBrowser.tabs[0], "Should have an inactive tab to switch to");
+
+ let hiddenPromise = promisePopupHidden(panel);
+
+ gBrowser.selectedTab = gBrowser.tabs[0];
+ yield hiddenPromise;
+ info("Panel closed");
+
+ gBrowser.selectedTab = oldTab;
} else if (panel) {
yield promisePopupShown(panel);
panel.hidePopup();
}
if (panel) {
panel = document.getElementById(panelId);
is(panel, null, "panel successfully removed from document after hiding");
@@ -172,16 +194,18 @@ add_task(function* testPageActionPopup()
yield extension.unload();
let node = document.getElementById(pageActionId);
is(node, null, "pageAction image removed from document");
let panel = document.getElementById(panelId);
is(panel, null, "pageAction panel removed from document");
+
+ yield BrowserTestUtils.removeTab(tab);
});
add_task(function* testPageActionSecurity() {
const URL = "chrome://browser/content/browser.xul";
let apis = ["browser_action", "page_action"];
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -1,15 +1,16 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const nsICookie = Components.interfaces.nsICookie;
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
Components.utils.import("resource://gre/modules/PluralForm.jsm");
Components.utils.import("resource://gre/modules/Services.jsm")
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
"resource://gre/modules/ContextualIdentityService.jsm");
var gCookiesWindow = {
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -1,11 +1,12 @@
[DEFAULT]
tags = devtools
subsuite = devtools
+skip-if = (os == 'linux' && debug && bits == 32)
support-files =
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
examples/bundle.js
examples/bundle.js.map
examples/doc-scripts.html
examples/doc-script-switching.html
--- a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
@@ -3,135 +3,142 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that autocompletion works as expected.
const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
const MAX_SUGGESTIONS = 15;
-const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
-const {CSSProperties, CSSValues} = getCSSKeywords();
+const {initCssProperties} = require("devtools/shared/fronts/css-properties");
// Test cases to test that autocompletion works correctly when enabled.
// Format:
// [
// key,
// {
// total: Number of suggestions in the popup (-1 if popup is closed),
// current: Index of selected suggestion,
// inserted: 1 to check whether the selected suggestion is inserted into the
// editor or not,
// entered: 1 if the suggestion is inserted and finalized
// }
// ]
-var TEST_CASES = [
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["Ctrl+Space", {total: 1, current: 0}],
- ["VK_LEFT"],
- ["VK_RIGHT"],
- ["VK_DOWN"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["Ctrl+Space", { total: getSuggestionNumberFor("font"), current: 0}],
- ["VK_END"],
- ["VK_RETURN"],
- ["b", {total: getSuggestionNumberFor("b"), current: 0}],
- ["a", {total: getSuggestionNumberFor("ba"), current: 0}],
- ["VK_DOWN", {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
- ["VK_TAB", {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
- ["VK_RETURN", {current: 1, inserted: 1, entered: 1}],
- ["b", {total: getSuggestionNumberFor("background", "b"), current: 0}],
- ["l", {total: getSuggestionNumberFor("background", "bl"), current: 0}],
- ["VK_TAB", {
- total: getSuggestionNumberFor("background", "bl"),
- current: 0, inserted: 1
- }],
- ["VK_DOWN", {
- total: getSuggestionNumberFor("background", "bl"),
- current: 1, inserted: 1
- }],
- ["VK_UP", {
- total: getSuggestionNumberFor("background", "bl"),
- current: 0,
- inserted: 1
- }],
- ["VK_TAB", {
- total: getSuggestionNumberFor("background", "bl"),
- current: 1,
- inserted: 1
- }],
- ["VK_TAB", {
- total: getSuggestionNumberFor("background", "bl"),
- current: 2,
- inserted: 1
- }],
- [";"],
- ["VK_RETURN"],
- ["c", {total: getSuggestionNumberFor("c"), current: 0}],
- ["o", {total: getSuggestionNumberFor("co"), current: 0}],
- ["VK_RETURN", {current: 0, inserted: 1}],
- ["r", {total: getSuggestionNumberFor("color", "r"), current: 0}],
- ["VK_RETURN", {current: 0, inserted: 1}],
- [";"],
- ["VK_LEFT"],
- ["VK_RIGHT"],
- ["VK_DOWN"],
- ["VK_RETURN"],
- ["b", {total: 2, current: 0}],
- ["u", {total: 1, current: 0}],
- ["VK_RETURN", {current: 0, inserted: 1}],
- ["{"],
- ["VK_HOME"],
- ["VK_DOWN"],
- ["VK_DOWN"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["VK_RIGHT"],
- ["Ctrl+Space", {total: 1, current: 0}],
-];
+
+function getTestCases(cssProperties) {
+ let keywords = getCSSKeywords(cssProperties);
+ let getSuggestionNumberFor = suggestionNumberGetter(keywords);
+
+ return [
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", {total: 1, current: 0}],
+ ["VK_LEFT"],
+ ["VK_RIGHT"],
+ ["VK_DOWN"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", { total: getSuggestionNumberFor("font"), current: 0}],
+ ["VK_END"],
+ ["VK_RETURN"],
+ ["b", {total: getSuggestionNumberFor("b"), current: 0}],
+ ["a", {total: getSuggestionNumberFor("ba"), current: 0}],
+ ["VK_DOWN", {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
+ ["VK_TAB", {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
+ ["VK_RETURN", {current: 1, inserted: 1, entered: 1}],
+ ["b", {total: getSuggestionNumberFor("background", "b"), current: 0}],
+ ["l", {total: getSuggestionNumberFor("background", "bl"), current: 0}],
+ ["VK_TAB", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 0, inserted: 1
+ }],
+ ["VK_DOWN", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 1, inserted: 1
+ }],
+ ["VK_UP", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 0,
+ inserted: 1
+ }],
+ ["VK_TAB", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 1,
+ inserted: 1
+ }],
+ ["VK_TAB", {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 2,
+ inserted: 1
+ }],
+ [";"],
+ ["VK_RETURN"],
+ ["c", {total: getSuggestionNumberFor("c"), current: 0}],
+ ["o", {total: getSuggestionNumberFor("co"), current: 0}],
+ ["VK_RETURN", {current: 0, inserted: 1}],
+ ["r", {total: getSuggestionNumberFor("color", "r"), current: 0}],
+ ["VK_RETURN", {current: 0, inserted: 1}],
+ [";"],
+ ["VK_LEFT"],
+ ["VK_RIGHT"],
+ ["VK_DOWN"],
+ ["VK_RETURN"],
+ ["b", {total: 2, current: 0}],
+ ["u", {total: 1, current: 0}],
+ ["VK_RETURN", {current: 0, inserted: 1}],
+ ["{"],
+ ["VK_HOME"],
+ ["VK_DOWN"],
+ ["VK_DOWN"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", {total: 1, current: 0}],
+ ];
+}
add_task(function* () {
let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+ let { cssProperties } = yield initCssProperties(panel._toolbox);
+ let testCases = getTestCases(cssProperties);
yield ui.selectStyleSheet(ui.editors[1].styleSheet);
let editor = yield ui.editors[1].getSourceEditor();
let sourceEditor = editor.sourceEditor;
let popup = sourceEditor.getAutocompletionPopup();
yield SimpleTest.promiseFocus(panel.panelWindow);
- for (let index in TEST_CASES) {
- yield testState(index, sourceEditor, popup, panel.panelWindow);
- yield checkState(index, sourceEditor, popup);
+ for (let index in testCases) {
+ yield testState(testCases, index, sourceEditor, popup, panel.panelWindow);
+ yield checkState(testCases, index, sourceEditor, popup);
}
});
-function testState(index, sourceEditor, popup, panelWindow) {
- let [key, details] = TEST_CASES[index];
+function testState(testCases, index, sourceEditor, popup, panelWindow) {
+ let [key, details] = testCases[index];
let entered;
if (details) {
entered = details.entered;
}
let mods = {};
info("pressing key " + key + " to get result: " +
- JSON.stringify(TEST_CASES[index]) + " for index " + index);
+ JSON.stringify(testCases[index]) + " for index " + index);
let evt = "after-suggest";
if (key == "Ctrl+Space") {
key = " ";
mods.ctrlKey = true;
} else if (key == "VK_RETURN" && entered) {
evt = "popup-hidden";
@@ -143,20 +150,20 @@ function testState(index, sourceEditor,
}
let ready = sourceEditor.once(evt);
EventUtils.synthesizeKey(key, mods, panelWindow);
return ready;
}
-function checkState(index, sourceEditor, popup) {
+function checkState(testCases, index, sourceEditor, popup) {
let deferred = defer();
executeSoon(() => {
- let [, details] = TEST_CASES[index];
+ let [, details] = testCases[index];
details = details || {};
let {total, current, inserted} = details;
if (total != undefined) {
ok(popup.isOpen, "Popup is open for index " + index);
is(total, popup.itemCount,
"Correct total suggestions for index " + index);
is(current, popup.selectedIndex,
@@ -190,34 +197,35 @@ function checkState(index, sourceEditor,
*
* @return {Object} An object with following properties:
* - CSSProperties {Array} Array of string containing all the possible
* CSS property names.
* - CSSValues {Object|Map} A map where key is the property name and
* value is an array of string containing all the possible
* CSS values the property can have.
*/
-function getCSSKeywords() {
- let cssProperties = getClientCssProperties();
+function getCSSKeywords(cssProperties) {
let props = {};
let propNames = cssProperties.getNames();
propNames.forEach(prop => {
props[prop] = cssProperties.getValues(prop).sort();
});
return {
CSSValues: props,
CSSProperties: propNames.sort()
};
}
/**
- * Returns the number of expected suggestions for the given property and value.
- * If the value is not null, returns the number of values starting with `value`.
- * Returns the number of properties starting with `property` otherwise.
+ * Returns a function that returns the number of expected suggestions for the given
+ * property and value. If the value is not null, returns the number of values starting
+ * with `value`. Returns the number of properties starting with `property` otherwise.
*/
-function getSuggestionNumberFor(property, value) {
- if (value == null) {
- return CSSProperties.filter(prop => prop.startsWith(property))
- .slice(0, MAX_SUGGESTIONS).length;
- }
- return CSSValues[property].filter(val => val.startsWith(value))
- .slice(0, MAX_SUGGESTIONS).length;
+function suggestionNumberGetter({CSSProperties, CSSValues}) {
+ return (property, value) => {
+ if (value == null) {
+ return CSSProperties.filter(prop => prop.startsWith(property))
+ .slice(0, MAX_SUGGESTIONS).length;
+ }
+ return CSSValues[property].filter(val => val.startsWith(value))
+ .slice(0, MAX_SUGGESTIONS).length;
+ };
}
--- a/devtools/server/actors/css-properties.js
+++ b/devtools/server/actors/css-properties.js
@@ -5,20 +5,16 @@
"use strict";
const { Cc, Ci } = require("chrome");
loader.lazyGetter(this, "DOMUtils", () => {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});
-loader.lazyGetter(this, "appInfo", () => {
- return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
-});
-
const protocol = require("devtools/shared/protocol");
const { ActorClassWithSpec, Actor } = protocol;
const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
const { CSS_PROPERTIES, CSS_TYPES } = require("devtools/shared/css/properties-db");
const { cssColors } = require("devtools/shared/css/color-db");
exports.CssPropertiesActor = ActorClassWithSpec(cssPropertiesSpec, {
typeName: "cssProperties",
@@ -27,25 +23,17 @@ exports.CssPropertiesActor = ActorClassW
Actor.prototype.initialize.call(this, conn);
this.parent = parent;
},
destroy() {
Actor.prototype.destroy.call(this);
},
- getCSSDatabase(clientBrowserVersion) {
- // If the client and server are both the same version of Firefox, do not return a
- // database, use the client-side css-properties-db.js.
- const serverBrowserVersion = appInfo.platformVersion.match(/^\d+/)[0];
-
- if (clientBrowserVersion !== 0 && clientBrowserVersion === serverBrowserVersion) {
- return {};
- }
-
+ getCSSDatabase() {
const properties = generateCssProperties();
const pseudoElements = DOMUtils.getCSSPseudoElementNames();
return { properties, pseudoElements };
}
});
/**
--- a/devtools/shared/fronts/css-properties.js
+++ b/devtools/shared/fronts/css-properties.js
@@ -200,19 +200,20 @@ const initCssProperties = Task.async(fun
return cachedCssProperties.get(client);
}
let db, front;
// Get the list dynamically if the cssProperties actor exists.
if (toolbox.target.hasActor("cssProperties")) {
front = CssPropertiesFront(client, toolbox.target.form);
- const serverDB = yield front.getCSSDatabase(getClientBrowserVersion(toolbox));
+ const serverDB = yield front.getCSSDatabase();
- // The serverDB will be blank if the browser versions match, so use the static list.
+ // Ensure the database was returned in a format that is understood.
+ // Older versions of the protocol could return a blank database.
if (!serverDB.properties && !serverDB.margin) {
db = CSS_PROPERTIES_DB;
} else {
db = serverDB;
}
} else {
// The target does not support this actor, so require a static list of supported
// properties.
@@ -245,26 +246,16 @@ function getCssProperties(toolbox) {
* the target.
* @return {CssProperties}
*/
function getClientCssProperties() {
return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
}
/**
- * Get the current browser version.
- * @returns {string} The browser version.
- */
-function getClientBrowserVersion(toolbox) {
- const regexResult = toolbox.win.navigator
- .userAgent.match(/Firefox\/(\d+)\.\d/);
- return Array.isArray(regexResult) ? regexResult[1] : "0";
-}
-
-/**
* Even if the target has the cssProperties actor, the returned data may not be in the
* same shape or have all of the data we need. This normalizes the data and fills in
* any missing information like color values.
*
* @return {Object} The normalized CSS database.
*/
function normalizeCssData(db) {
if (db !== CSS_PROPERTIES_DB) {
--- a/devtools/shared/specs/css-properties.js
+++ b/devtools/shared/specs/css-properties.js
@@ -1,22 +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";
-const { Arg, RetVal, generateActorSpec } = require("devtools/shared/protocol");
+const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
const cssPropertiesSpec = generateActorSpec({
typeName: "cssProperties",
methods: {
getCSSDatabase: {
- request: {
- clientBrowserVersion: Arg(0, "string"),
- },
+ request: {},
response: RetVal("json"),
}
}
});
exports.cssPropertiesSpec = cssPropertiesSpec;
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -5,16 +5,21 @@ tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
support-files =
exposeLoader.js
[test_assert.js]
[test_csslexer.js]
[test_css-properties-db.js]
+# This test only enforces that the CSS database is up to date with nightly. The DB is
+# only used when inspecting a target that doesn't support the getCSSDatabase actor.
+# CSS properties are behind compile-time flags, and there is no automatic rebuild
+# process for uplifts, so this test breaks on uplift.
+run-if = nightly_build
[test_fetch-bom.js]
[test_fetch-chrome.js]
[test_fetch-file.js]
[test_fetch-http.js]
[test_fetch-resource.js]
[test_flatten.js]
[test_indentation.js]
[test_independent_loaders.js]
--- a/dom/browser-element/mochitest/browserElementTestHelpers.js
+++ b/dom/browser-element/mochitest/browserElementTestHelpers.js
@@ -60,18 +60,21 @@ const browserElementTestHelpers = {
setClipboardPlainTextOnlyPref: function(value) {
this._setPref('clipboard.plainTextOnly', value);
},
setEnabledPref: function(value) {
this._setPref('dom.mozBrowserFramesEnabled', value);
},
- setAccessibleCaretEnabledPref: function(value) {
- this._setPref('layout.accessiblecaret.enabled', value);
+ setupAccessibleCaretPref: function() {
+ this._setPref('layout.accessiblecaret.enabled', true);
+ // Disable hide carets for mouse input for select-all tests so that we can
+ // get mozbrowsercaretstatechanged events.
+ this._setPref('layout.accessiblecaret.hide_carets_for_mouse_input', false);
},
getOOPByDefaultPref: function() {
return this._getBoolPref("dom.ipc.browser_frames.oop_by_default");
},
addPermission: function() {
this.lockTestReady();
--- a/dom/browser-element/mochitest/browserElement_CopyPaste.js
+++ b/dom/browser-element/mochitest/browserElement_CopyPaste.js
@@ -2,17 +2,17 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that "cut, copy, paste, selectall" and caretstatechanged event works from inside an <iframe mozbrowser>.
"use strict";
SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("untriaged");
browserElementTestHelpers.setEnabledPref(true);
-browserElementTestHelpers.setAccessibleCaretEnabledPref(true);
+browserElementTestHelpers.setupAccessibleCaretPref();
browserElementTestHelpers.addPermission();
const { Services } = SpecialPowers.Cu.import('resource://gre/modules/Services.jsm');
var gTextarea = null;
var mm;
var iframeOuter;
var iframeInner;
var state = 0;
--- a/dom/browser-element/mochitest/browserElement_Manifestchange.js
+++ b/dom/browser-element/mochitest/browserElement_Manifestchange.js
@@ -37,17 +37,17 @@ function runTest() {
iframe1.addEventListener('mozbrowsermanifestchange', function(e) {
numManifestChanges++;
if (numManifestChanges == 1) {
is(e.detail.href, 'manifest.1', 'manifest.1 matches');
- // We should recieve manifestchange events when the user creates new
+ // We should receive manifestchange events when the user creates new
// manifests
SpecialPowers.getBrowserFrameMessageManager(iframe1)
.loadFrameScript("data:,content.document.title='New title';",
/* allowDelayedLoad = */ false);
SpecialPowers.getBrowserFrameMessageManager(iframe1)
.loadFrameScript("data:,content.document.head.insertAdjacentHTML('beforeend', '<link rel=manifest href=manifest.2>')",
/* allowDelayedLoad = */ false);
@@ -81,16 +81,16 @@ function runTest() {
});
iframe3.addEventListener('mozbrowsermanifestchange', function(e) {
ok(false, 'Should not get a manifestchange event for iframe3.');
});
iframe1.src = createHtml(createManifest('manifest.1'));
- // We should not recieve manifest change events for either of the below iframes
+ // We should not receive manifest change events for either of the below iframes
iframe2.src = createHtml(createManifest('manifest.1'));
iframe3.src = createHtml(createManifest('manifest.1'));
}
addEventListener('testready', runTest);
--- a/dom/events/test/test_all_synthetic_events.html
+++ b/dom/events/test/test_all_synthetic_events.html
@@ -439,16 +439,20 @@ const kEventConstructors = {
RecordErrorEvent: { create: function (aName, aProps) {
return new RecordErrorEvent(aName, aProps);
},
},
RTCDataChannelEvent: { create: function (aName, aProps) {
return new RTCDataChannelEvent(aName, aProps);
},
},
+ RTCDTMFToneChangeEvent: { create: function (aName, aProps) {
+ return new RTCDTMFToneChangeEvent(aName, aProps);
+ },
+ },
RTCPeerConnectionIceEvent: { create: function (aName, aProps) {
return new RTCPeerConnectionIceEvent(aName, aProps);
},
},
RTCTrackEvent: {
// Difficult to test required arguments.
},
ScrollAreaEvent: { create: function (aName, aProps) {
--- a/dom/grid/GridLines.cpp
+++ b/dom/grid/GridLines.cpp
@@ -63,16 +63,17 @@ GridLines::IndexedGetter(uint32_t aIndex
}
void
GridLines::SetLineInfo(const ComputedGridTrackInfo* aTrackInfo,
const ComputedGridLineInfo* aLineInfo,
const nsTArray<RefPtr<GridArea>>& aAreas,
bool aIsRow)
{
+ MOZ_ASSERT(aLineInfo);
mLines.Clear();
if (!aTrackInfo) {
return;
}
uint32_t trackCount = aTrackInfo->mEndFragmentTrack -
aTrackInfo->mStartFragmentTrack;
@@ -91,19 +92,17 @@ GridLines::SetLineInfo(const ComputedGri
i++) {
uint32_t line1Index = i + 1;
startOfNextTrack = (i < aTrackInfo->mEndFragmentTrack) ?
aTrackInfo->mPositions[i] :
lastTrackEdge;
nsTArray<nsString> lineNames;
- if (aLineInfo) {
- lineNames = aLineInfo->mNames.SafeElementAt(i, nsTArray<nsString>());
- }
+ lineNames = aLineInfo->mNames.SafeElementAt(i, nsTArray<nsString>());
// Add in names from grid areas where this line is used as a boundary.
for (auto area : aAreas) {
bool haveNameToAdd = false;
nsAutoString nameToAdd;
area->GetName(nameToAdd);
if (aIsRow) {
if (area->RowStart() == line1Index) {
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -932,21 +932,21 @@ public:
// With buffering heuristics we will remain in the buffering state if
// we've not decoded enough data to begin playback, or if we've not
// downloaded a reasonable amount of data inside our buffering time.
if (Reader()->UseBufferingHeuristics()) {
TimeDuration elapsed = now - mBufferingStart;
bool isLiveStream = Resource()->IsLiveStream();
if ((isLiveStream || !mMaster->CanPlayThrough()) &&
- elapsed < TimeDuration::FromSeconds(mMaster->mBufferingWait * mMaster->mPlaybackRate) &&
- mMaster->HasLowBufferedData(mMaster->mBufferingWait * USECS_PER_S) &&
+ elapsed < TimeDuration::FromSeconds(mBufferingWait * mMaster->mPlaybackRate) &&
+ mMaster->HasLowBufferedData(mBufferingWait * USECS_PER_S) &&
Resource()->IsExpectingMoreData()) {
SLOG("Buffering: wait %ds, timeout in %.3lfs",
- mMaster->mBufferingWait, mMaster->mBufferingWait - elapsed.ToSeconds());
+ mBufferingWait, mBufferingWait - elapsed.ToSeconds());
mMaster->ScheduleStateMachineIn(USECS_PER_S);
return;
}
} else if (mMaster->OutOfDecodedAudio() || mMaster->OutOfDecodedVideo()) {
MOZ_ASSERT(Reader()->IsWaitForDataSupported(),
"Don't yet have a strategy for non-heuristic + non-WaitForData");
mMaster->DispatchDecodeTasksIfNeeded();
MOZ_ASSERT(mMaster->mMinimizePreroll ||
@@ -1010,16 +1010,20 @@ public:
seekJob.mTarget = aTarget;
RefPtr<MediaDecoder::SeekPromise> p = seekJob.mPromise.Ensure(__func__);
mMaster->InitiateSeek(Move(seekJob));
return p.forget();
}
private:
TimeStamp mBufferingStart;
+
+ // The maximum number of second we spend buffering when we are short on
+ // unbuffered data.
+ const uint32_t mBufferingWait = 15;
};
class MediaDecoderStateMachine::CompletedState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit CompletedState(Master* aPtr) : StateObject(aPtr) {}
@@ -1188,19 +1192,16 @@ MediaDecoderStateMachine::MediaDecoderSt
INIT_CANONICAL(mPlaybackOffset, 0),
INIT_CANONICAL(mIsAudioDataAudible, false)
{
MOZ_COUNT_CTOR(MediaDecoderStateMachine);
NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
InitVideoQueuePrefs();
- mBufferingWait = 15;
- mLowDataThresholdUsecs = detail::LOW_DATA_THRESHOLD_USECS;
-
#ifdef XP_WIN
// Ensure high precision timers are enabled on Windows, otherwise the state
// machine isn't woken up at reliable intervals to set the next frame,
// and we drop frames while painting. Note that multiple calls to this
// function per-process is OK, provided each call is matched by a corresponding
// timeEndPeriod() call.
timeBeginPeriod(1);
#endif
@@ -2458,17 +2459,17 @@ bool MediaDecoderStateMachine::OutOfDeco
return IsAudioDecoding() && !AudioQueue().IsFinished() &&
AudioQueue().GetSize() == 0 &&
!mMediaSink->HasUnplayedFrames(TrackInfo::kAudioTrack);
}
bool MediaDecoderStateMachine::HasLowBufferedData()
{
MOZ_ASSERT(OnTaskQueue());
- return HasLowBufferedData(mLowDataThresholdUsecs);
+ return HasLowBufferedData(detail::LOW_DATA_THRESHOLD_USECS);
}
bool MediaDecoderStateMachine::HasLowBufferedData(int64_t aUsecs)
{
MOZ_ASSERT(OnTaskQueue());
MOZ_ASSERT(mState >= DECODER_STATE_DECODING,
"Must have loaded first frame for mBuffered to be valid");
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -641,21 +641,16 @@ private:
// The end time of the last decoded video frame. Used to check if we are low
// on decoded video data.
int64_t mDecodedVideoEndTime;
// Playback rate. 1.0 : normal speed, 0.5 : two times slower.
double mPlaybackRate;
- // The maximum number of second we spend buffering when we are short on
- // unbuffered data.
- uint32_t mBufferingWait;
- int64_t mLowDataThresholdUsecs;
-
// If we've got more than this number of decoded video frames waiting in
// the video queue, we will not decode any more video frames until some have
// been consumed by the play state machine thread.
// Must hold monitor.
uint32_t GetAmpleVideoFrames() const;
// Low audio threshold. If we've decoded less than this much audio we
// consider our audio decode "behind", and we may skip video decoding
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -21,27 +21,29 @@ const PC_OBS_CONTRACT = "@mozilla.org/do
const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1";
const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1";
const PC_RECEIVER_CONTRACT = "@mozilla.org/dom/rtpreceiver;1";
const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
+const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1";
const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}");
const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}");
const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}");
const PC_COREQUEST_CID = Components.ID("{74b2122d-65a8-4824-aa9e-3d664cb75dc2}");
+const PC_DTMF_SENDER_CID = Components.ID("{3610C242-654E-11E6-8EC0-6D1BE389A607}");
// Global list of PeerConnection objects, so they can be cleaned up when
// a page is torn down. (Maps inner window ID to an array of PC objects).
function GlobalPCList() {
this._list = {};
this._networkdown = false; // XXX Need to query current state somehow
this._lifecycleobservers = {};
this._nextId = 1;
@@ -1061,16 +1063,24 @@ RTCPeerConnection.prototype = {
this._checkClosed();
var i = this._senders.indexOf(sender);
if (i >= 0) {
this._senders.splice(i, 1);
this._impl.removeTrack(sender.track); // fires negotiation needed
}
},
+ _insertDTMF: function(sender, tones, duration, interToneGap) {
+ return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
+ },
+
+ _getDTMFToneBuffer: function(sender) {
+ return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
+ },
+
_replaceTrack: function(sender, withTrack) {
// TODO: Do a (sender._stream.getTracks().indexOf(track) < 0) check
// on both track args someday.
//
// The proposed API will be that both tracks must already be in the same
// stream. However, since our MediaStreams currently are limited to one
// track per type, we allow replacement with an outside track not already
// in the same stream.
@@ -1553,16 +1563,23 @@ PeerConnectionObserver.prototype = {
foundIceCandidate: function(cand) {
this.dispatchEvent(new this._dompc._win.RTCPeerConnectionIceEvent("icecandidate",
{ candidate: cand } ));
},
notifyDataChannel: function(channel) {
this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
{ channel: channel }));
+ },
+
+ onDTMFToneChange: function(trackId, tone) {
+ var pc = this._dompc;
+ var sender = pc._senders.find(sender => sender.track.id == trackId)
+ sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
+ { tone: tone }));
}
};
function RTCPeerConnectionStatic() {
}
RTCPeerConnectionStatic.prototype = {
classDescription: "RTCPeerConnectionStatic",
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
@@ -1576,20 +1593,68 @@ RTCPeerConnectionStatic.prototype = {
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
},
registerPeerConnectionLifecycleCallback: function(cb) {
_globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb);
},
};
+function RTCDTMFSender(sender) {
+ this._sender = sender;
+ this.duration = 100;
+ this.interToneGap = 70;
+}
+RTCDTMFSender.prototype = {
+ classDescription: "RTCDTMFSender",
+ classID: PC_DTMF_SENDER_CID,
+ contractID: PC_DTMF_SENDER_CONTRACT,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
+
+ get toneBuffer() {
+ return this._sender._pc._getDTMFToneBuffer(this._sender);
+ },
+
+ get ontonechange() {
+ return this.__DOM_IMPL__.getEventHandler("ontonechange");
+ },
+
+ set ontonechange(handler) {
+ this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
+ },
+
+ insertDTMF: function(tones, duration, interToneGap) {
+ this._sender._pc._checkClosed();
+
+ if (this._sender._pc._senders.indexOf(this._sender.__DOM_IMPL__) == -1) {
+ throw new this._sender._pc._win.DOMException("RTCRtpSender is stopped",
+ "InvalidStateError");
+ }
+
+ this.duration = Math.max(40, Math.min(duration, 6000));
+
+ if (interToneGap < 30) interToneGap = 30;
+ this.interToneGap = interToneGap;
+
+ tones = tones.toUpperCase();
+
+ if (tones.match(/[^0-9A-D#*,]/)) {
+ throw new this._sender._pc._win.DOMException("Invalid DTMF characters",
+ "InvalidCharacterError");
+ }
+
+ this._sender._pc._insertDTMF(this._sender, tones, duration, interToneGap);
+ },
+};
+
function RTCRtpSender(pc, track, stream) {
this._pc = pc;
this.track = track;
this._stream = stream;
+ this.dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
}
RTCRtpSender.prototype = {
classDescription: "RTCRtpSender",
classID: PC_SENDER_CID,
contractID: PC_SENDER_CONTRACT,
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
replaceTrack: function(withTrack) {
@@ -1627,16 +1692,17 @@ CreateOfferRequest.prototype = {
classDescription: "CreateOfferRequest",
classID: PC_COREQUEST_CID,
contractID: PC_COREQUEST_CONTRACT,
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
[GlobalPCList,
+ RTCDTMFSender,
RTCIceCandidate,
RTCSessionDescription,
RTCPeerConnection,
RTCPeerConnectionStatic,
RTCRtpReceiver,
RTCRtpSender,
RTCStatsReport,
PeerConnectionObserver,
--- a/dom/media/PeerConnection.manifest
+++ b/dom/media/PeerConnection.manifest
@@ -3,19 +3,21 @@ component {d1748d4c-7f6a-4dc5-add6-d55b7
component {02b9970c-433d-4cc2-923d-f7028ac66073} PeerConnection.js
component {1775081b-b62d-4954-8ffe-a067bbf508a7} PeerConnection.js
component {7293e901-2be3-4c02-b4bd-cbef6fc24f78} PeerConnection.js
component {7fe6e18b-0da3-4056-bf3b-440ef3809e06} PeerConnection.js
component {0fb47c47-a205-4583-a9fc-cbadf8c95880} PeerConnection.js
component {4fff5d46-d827-4cd4-a970-8fd53977440e} PeerConnection.js
component {d974b814-8fde-411c-8c45-b86791b81030} PeerConnection.js
component {74b2122d-65a8-4824-aa9e-3d664cb75dc2} PeerConnection.js
+component {3610C242-654E-11E6-8EC0-6D1BE389A607} PeerConnection.js
contract @mozilla.org/dom/peerconnection;1 {bdc2e533-b308-4708-ac8e-a8bfade6d851}
contract @mozilla.org/dom/peerconnectionobserver;1 {d1748d4c-7f6a-4dc5-add6-d55b7678537e}
+contract @mozilla.org/dom/rtcdtmfsender;1 {3610C242-654E-11E6-8EC0-6D1BE389A607}
contract @mozilla.org/dom/rtcicecandidate;1 {02b9970c-433d-4cc2-923d-f7028ac66073}
contract @mozilla.org/dom/rtcsessiondescription;1 {1775081b-b62d-4954-8ffe-a067bbf508a7}
contract @mozilla.org/dom/peerconnectionmanager;1 {7293e901-2be3-4c02-b4bd-cbef6fc24f78}
contract @mozilla.org/dom/rtcstatsreport;1 {7fe6e18b-0da3-4056-bf3b-440ef3809e06}
contract @mozilla.org/dom/peerconnectionstatic;1 {0fb47c47-a205-4583-a9fc-cbadf8c95880}
contract @mozilla.org/dom/rtpsender;1 {4fff5d46-d827-4cd4-a970-8fd53977440e}
contract @mozilla.org/dom/rtpreceiver;1 {d974b814-8fde-411c-8c45-b86791b81030}
contract @mozilla.org/dom/createofferrequest;1 {74b2122d-65a8-4824-aa9e-3d664cb75dc2}
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -134,16 +134,17 @@ skip-if = (android_version == '18' && de
# [test_peerConnection_certificates.html] # bug 1180968
[test_peerConnection_close.html]
[test_peerConnection_closeDuringIce.html]
[test_peerConnection_constructedStream.html]
skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
[test_peerConnection_errorCallbacks.html]
[test_peerConnection_iceFailure.html]
skip-if = os == 'linux' || os == 'mac' || os == 'win' || android_version == '18' # (Bug 1180388 for win, mac and linux), android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_insertDTMF.html]
[test_peerConnection_forwarding_basicAudioVideoCombined.html]
skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
[test_peerConnection_noTrickleAnswer.html]
[test_peerConnection_noTrickleOffer.html]
[test_peerConnection_noTrickleOfferAnswer.html]
[test_peerConnection_offerRequiresReceiveAudio.html]
[test_peerConnection_offerRequiresReceiveVideo.html]
[test_peerConnection_offerRequiresReceiveVideoAudio.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_insertDTMF.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+createHTML({
+ bug: "1291715",
+ title: "Test insertDTMF on sender",
+ visible: true
+});
+
+function insertdtmftest(pc) {
+ ok(pc.getSenders().length > 0, "have senders");
+ var sender = pc.getSenders()[0];
+ ok(sender.dtmf, "sender has dtmf object");
+
+ ok(sender.dtmf.toneBuffer === "", "sender should start with empty tonebuffer");
+
+ sender.dtmf.insertDTMF("A", 10);
+ is(sender.dtmf.duration, 40, "minimum duration is 40");
+ sender.dtmf.insertDTMF("A", 10000);
+ is(sender.dtmf.duration, 6000, "maximum duration is 6000");
+ sender.dtmf.insertDTMF("A", 70, 10);
+ is(sender.dtmf.duration, 70, "default duration is 70");
+ is(sender.dtmf.interToneGap, 30, "minimum interToneGap is 30");
+ sender.dtmf.insertDTMF("", 100, 40);
+ is(sender.dtmf.duration, 100, "duration is 70");
+ is(sender.dtmf.interToneGap, 40, "interToneGap is 40");
+
+ var threw = false;
+ try {
+ sender.dtmf.insertDTMF("bad tones");
+ } catch (ex) {
+ threw = true;
+ is(ex.code, DOMException.INVALID_CHARACTER_ERR, "Expected InvalidCharacterError");
+ }
+ ok(threw, "Expected exception");
+
+ sender.dtmf.insertDTMF("A");
+ sender.dtmf.insertDTMF("B");
+ ok(sender.dtmf.toneBuffer.indexOf("A") == -1, "calling insertDTMF should replace current characters");
+
+ sender.dtmf.insertDTMF("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ ok(sender.dtmf.toneBuffer.indexOf("A") != -1, "lowercase characters should be normalized");
+
+ pc.removeTrack(sender);
+ threw = false;
+ try {
+ sender.dtmf.insertDTMF("AAA");
+ } catch (ex) {
+ threw = true;
+ is(ex.code, DOMException.INVALID_STATE_ERR, "Expected InvalidStateError");
+ }
+ ok(threw, "Expected exception");
+}
+
+runNetworkTest(() => {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test sender dtmf.
+ test.chain.append([
+ function PC_LOCAL_INSERT_DTMF(test) {
+ // We want to call removeTrack
+ test.pcLocal.expectNegotiationNeeded();
+ return insertdtmftest(test.pcLocal._pc);
+ }
+ ]);
+
+ var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+ return pushPrefs(['media.peerconnection.dtmf.enabled', true])
+ .then(() => { test.run() })
+ .catch(e => ok(false, "unexpected failure: " + e));
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -996,16 +996,20 @@ var interfaceNamesInGlobalScope =
"Response",
// IMPORTANT: Do not change this list without review from a DOM peer!
"RGBColor",
// IMPORTANT: Do not change this list without review from a DOM peer!
"RTCCertificate",
// IMPORTANT: Do not change this list without review from a DOM peer!
"RTCDataChannelEvent",
// IMPORTANT: Do not change this list without review from a DOM peer!
+ "RTCDTMFSender",
+// IMPORTANT: Do not change this list without review from a DOM peer!
+ "RTCDTMFToneChangeEvent",
+// IMPORTANT: Do not change this list without review from a DOM peer!
"RTCIceCandidate",
// IMPORTANT: Do not change this list without review from a DOM peer!
"RTCPeerConnection",
// IMPORTANT: Do not change this list without review from a DOM peer!
"RTCPeerConnectionIceEvent",
// IMPORTANT: Do not change this list without review from a DOM peer!
"RTCRtpReceiver",
// IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/PeerConnectionImpl.webidl
+++ b/dom/webidl/PeerConnectionImpl.webidl
@@ -40,16 +40,22 @@ interface PeerConnectionImpl {
void getStats(MediaStreamTrack? selector);
/* Adds the tracks created by GetUserMedia */
[Throws]
void addTrack(MediaStreamTrack track, MediaStream... streams);
[Throws]
void removeTrack(MediaStreamTrack track);
[Throws]
+ void insertDTMF(RTCRtpSender sender, DOMString tones,
+ optional unsigned long duration = 100,
+ optional unsigned long interToneGap = 70);
+ [Throws]
+ DOMString getDTMFToneBuffer(RTCRtpSender sender);
+ [Throws]
void replaceTrack(MediaStreamTrack thisTrack, MediaStreamTrack withTrack);
[Throws]
void setParameters(MediaStreamTrack track,
optional RTCRtpParameters parameters);
[Throws]
RTCRtpParameters getParameters(MediaStreamTrack track);
[Throws]
void closeStreams();
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -39,9 +39,12 @@ interface PeerConnectionObserver
/* Notification of one of several types of state changed */
void onStateChange(PCObserverStateType state);
/* Changes to MediaStreamTracks */
void onAddStream(MediaStream stream);
void onRemoveStream(MediaStream stream);
void onAddTrack(MediaStreamTrack track, sequence<MediaStream> streams);
void onRemoveTrack(MediaStreamTrack track);
+
+ /* DTMF callback */
+ void onDTMFToneChange(DOMString trackId, DOMString tone);
};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCDTMFSender.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://www.w3.org/TR/webrtc/#rtcdtmfsender
+ */
+
+[JSImplementation="@mozilla.org/dom/rtcdtmfsender;1"]
+interface RTCDTMFSender : EventTarget {
+ void insertDTMF(DOMString tones,
+ optional unsigned long duration = 100,
+ optional unsigned long interToneGap = 70);
+ attribute EventHandler ontonechange;
+ readonly attribute DOMString toneBuffer;
+ readonly attribute unsigned long duration;
+ readonly attribute unsigned long interToneGap;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCDTMFToneChangeEvent.webidl
@@ -0,0 +1,8 @@
+[Constructor(DOMString type, optional RTCDTMFToneChangeEventInit eventInitDict)]
+interface RTCDTMFToneChangeEvent : Event {
+ readonly attribute DOMString tone;
+};
+
+dictionary RTCDTMFToneChangeEventInit : EventInit {
+ DOMString tone = "";
+};
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -68,9 +68,11 @@ dictionary RTCRtpParameters {
[Pref="media.peerconnection.enabled",
JSImplementation="@mozilla.org/dom/rtpsender;1"]
interface RTCRtpSender {
readonly attribute MediaStreamTrack track;
Promise<void> setParameters (optional RTCRtpParameters parameters);
RTCRtpParameters getParameters();
Promise<void> replaceTrack(MediaStreamTrack track);
+ [Pref="media.peerconnection.dtmf.enabled"]
+ readonly attribute RTCDTMFSender? dtmf;
};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -644,16 +644,17 @@ if CONFIG['MOZ_WEBRTC']:
'DataChannel.webidl',
'MediaStreamList.webidl',
'PeerConnectionImpl.webidl',
'PeerConnectionImplEnums.webidl',
'PeerConnectionObserver.webidl',
'PeerConnectionObserverEnums.webidl',
'RTCCertificate.webidl',
'RTCConfiguration.webidl',
+ 'RTCDTMFSender.webidl',
'RTCIceCandidate.webidl',
'RTCIdentityAssertion.webidl',
'RTCIdentityProvider.webidl',
'RTCPeerConnection.webidl',
'RTCPeerConnectionStatic.webidl',
'RTCRtpReceiver.webidl',
'RTCRtpSender.webidl',
'RTCSessionDescription.webidl',
@@ -840,16 +841,17 @@ GENERATED_EVENTS_WEBIDL_FILES = [
'UserProximityEvent.webidl',
'USSDReceivedEvent.webidl',
'WebGLContextEvent.webidl',
]
if CONFIG['MOZ_WEBRTC']:
GENERATED_EVENTS_WEBIDL_FILES += [
'RTCDataChannelEvent.webidl',
+ 'RTCDTMFToneChangeEvent.webidl',
'RTCPeerConnectionIceEvent.webidl',
'RTCTrackEvent.webidl',
]
if CONFIG['MOZ_WEBSPEECH']:
GENERATED_EVENTS_WEBIDL_FILES += [
'SpeechRecognitionEvent.webidl',
'SpeechSynthesisErrorEvent.webidl',
--- a/gfx/angle/src/libANGLE/renderer/d3d/d3d11/Framebuffer11.cpp
+++ b/gfx/angle/src/libANGLE/renderer/d3d/d3d11/Framebuffer11.cpp
@@ -177,17 +177,20 @@ gl::Error Framebuffer11::invalidateBase(
{
// Handle color attachments
ASSERT((attachments[i] >= GL_COLOR_ATTACHMENT0 && attachments[i] <= GL_COLOR_ATTACHMENT15) ||
(attachments[i] == GL_COLOR));
size_t colorIndex =
(attachments[i] == GL_COLOR ? 0u : (attachments[i] - GL_COLOR_ATTACHMENT0));
auto colorAttachment = mState.getColorAttachment(colorIndex);
- ANGLE_TRY(invalidateAttachment(colorAttachment));
+ if (colorAttachment)
+ {
+ ANGLE_TRY(invalidateAttachment(colorAttachment));
+ }
break;
}
}
}
bool discardDepth = false;
bool discardStencil = false;
--- a/gfx/skia/generate_mozbuild.py
+++ b/gfx/skia/generate_mozbuild.py
@@ -120,16 +120,17 @@ if CONFIG['MOZ_TREE_FREETYPE']:
# Suppress warnings in third-party code.
if CONFIG['GNU_CXX'] or CONFIG['CLANG_CL']:
CXXFLAGS += [
'-Wno-deprecated-declarations',
'-Wno-overloaded-virtual',
'-Wno-shadow',
'-Wno-sign-compare',
+ '-Wno-unreachable-code',
'-Wno-unused-function',
]
if CONFIG['GNU_CXX'] and not CONFIG['CLANG_CXX'] and not CONFIG['CLANG_CL']:
CXXFLAGS += [
'-Wno-logical-op',
'-Wno-maybe-uninitialized',
]
if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
--- a/gfx/skia/moz.build
+++ b/gfx/skia/moz.build
@@ -652,16 +652,17 @@ if CONFIG['MOZ_TREE_FREETYPE']:
# Suppress warnings in third-party code.
if CONFIG['GNU_CXX'] or CONFIG['CLANG_CL']:
CXXFLAGS += [
'-Wno-deprecated-declarations',
'-Wno-overloaded-virtual',
'-Wno-shadow',
'-Wno-sign-compare',
+ '-Wno-unreachable-code',
'-Wno-unused-function',
]
if CONFIG['GNU_CXX'] and not CONFIG['CLANG_CXX'] and not CONFIG['CLANG_CL']:
CXXFLAGS += [
'-Wno-logical-op',
'-Wno-maybe-uninitialized',
]
if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
--- a/layout/base/AccessibleCaretManager.cpp
+++ b/layout/base/AccessibleCaretManager.cpp
@@ -191,16 +191,24 @@ AccessibleCaretManager::OnSelectionChang
// For mouse input we don't want to show the carets.
if (sHideCaretsForMouseInput &&
mLastInputSource == nsIDOMMouseEvent::MOZ_SOURCE_MOUSE) {
HideCarets();
return NS_OK;
}
+ // No need to show the carets for select all action when we want to hide
+ // the carets for mouse input.
+ if (sHideCaretsForMouseInput &&
+ (aReason & nsISelectionListener::SELECTALL_REASON)) {
+ HideCarets();
+ return NS_OK;
+ }
+
UpdateCarets();
return NS_OK;
}
void
AccessibleCaretManager::HideCarets()
{
if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -223,16 +223,31 @@ nsBlockFrame::InitDebugFlags()
#ifdef DEBUG
const char* nsBlockFrame::kReflowCommandType[] = {
"ContentChanged",
"StyleChanged",
"ReflowDirty",
"Timeout",
"UserDefined",
};
+
+const char*
+nsBlockFrame::LineReflowStatusToString(LineReflowStatus aLineReflowStatus) const
+{
+ switch (aLineReflowStatus) {
+ case LineReflowStatus::OK: return "LINE_REFLOW_OK";
+ case LineReflowStatus::Stop: return "LINE_REFLOW_STOP";
+ case LineReflowStatus::RedoNoPull: return "LINE_REFLOW_REDO_NO_PULL";
+ case LineReflowStatus::RedoMoreFloats: return "LINE_REFLOW_REDO_MORE_FLOATS";
+ case LineReflowStatus::RedoNextBand: return "LINE_REFLOW_REDO_NEXT_BAND";
+ case LineReflowStatus::Truncated: return "LINE_REFLOW_TRUNCATED";
+ }
+ return "unknown";
+}
+
#endif
#ifdef REFLOW_STATUS_COVERAGE
static void
RecordReflowStatus(bool aChildIsBlock, nsReflowStatus aFrameReflowStatus)
{
static uint32_t record[2];
@@ -3758,19 +3773,19 @@ nsBlockFrame::ReflowInlineFrames(BlockRe
lineLayout.ForceBreakAtPosition(forceBreakInFrame, forceBreakOffset);
}
DoReflowInlineFrames(aState, lineLayout, aLine,
floatAvailableSpace, availableSpaceHeight,
&floatManagerState, aKeepReflowGoing,
&lineReflowStatus, allowPullUp);
lineLayout.EndLineReflow();
- if (LINE_REFLOW_REDO_NO_PULL == lineReflowStatus ||
- LINE_REFLOW_REDO_MORE_FLOATS == lineReflowStatus ||
- LINE_REFLOW_REDO_NEXT_BAND == lineReflowStatus) {
+ if (LineReflowStatus::RedoNoPull == lineReflowStatus ||
+ LineReflowStatus::RedoMoreFloats == lineReflowStatus ||
+ LineReflowStatus::RedoNextBand == lineReflowStatus) {
if (lineLayout.NeedsBackup()) {
NS_ASSERTION(!forceBreakInFrame, "Backing up twice; this should never be necessary");
// If there is no saved break position, then this will set
// set forceBreakInFrame to null and we won't back up, which is
// correct.
forceBreakInFrame =
lineLayout.GetLastOptionalBreakPosition(&forceBreakOffset, &forceBreakPriority);
} else {
@@ -3778,41 +3793,33 @@ nsBlockFrame::ReflowInlineFrames(BlockRe
}
// restore the float manager state
aState.mReflowInput.mFloatManager->PopState(&floatManagerState);
// Clear out float lists
aState.mCurrentLineFloats.DeleteAll();
aState.mBelowCurrentLineFloats.DeleteAll();
}
- // Don't allow pullup on a subsequent LINE_REFLOW_REDO_NO_PULL pass
+ // Don't allow pullup on a subsequent LineReflowStatus::RedoNoPull pass
allowPullUp = false;
- } while (LINE_REFLOW_REDO_NO_PULL == lineReflowStatus);
- } while (LINE_REFLOW_REDO_MORE_FLOATS == lineReflowStatus);
- } while (LINE_REFLOW_REDO_NEXT_BAND == lineReflowStatus);
+ } while (LineReflowStatus::RedoNoPull == lineReflowStatus);
+ } while (LineReflowStatus::RedoMoreFloats == lineReflowStatus);
+ } while (LineReflowStatus::RedoNextBand == lineReflowStatus);
}
void
nsBlockFrame::PushTruncatedLine(BlockReflowInput& aState,
line_iterator aLine,
bool* aKeepReflowGoing)
{
PushLines(aState, aLine.prev());
*aKeepReflowGoing = false;
NS_FRAME_SET_INCOMPLETE(aState.mReflowStatus);
}
-#ifdef DEBUG
-static const char* LineReflowStatusNames[] = {
- "LINE_REFLOW_OK", "LINE_REFLOW_STOP", "LINE_REFLOW_REDO_NO_PULL",
- "LINE_REFLOW_REDO_MORE_FLOATS",
- "LINE_REFLOW_REDO_NEXT_BAND", "LINE_REFLOW_TRUNCATED"
-};
-#endif
-
void
nsBlockFrame::DoReflowInlineFrames(BlockReflowInput& aState,
nsLineLayout& aLineLayout,
line_iterator aLine,
nsFlowAreaRect& aFloatAvailableSpace,
nscoord& aAvailableSpaceHeight,
nsFloatManager::SavedState*
aFloatStateBeforeLine,
@@ -3869,65 +3876,65 @@ nsBlockFrame::DoReflowInlineFrames(Block
(NS_BLOCK_HAS_FIRST_LETTER_STYLE & mState)) {
aLineLayout.SetFirstLetterStyleOK(true);
}
NS_ASSERTION(!((NS_BLOCK_HAS_FIRST_LETTER_CHILD & mState) &&
GetPrevContinuation()),
"first letter child bit should only be on first continuation");
// Reflow the frames that are already on the line first
- LineReflowStatus lineReflowStatus = LINE_REFLOW_OK;
+ LineReflowStatus lineReflowStatus = LineReflowStatus::OK;
int32_t i;
nsIFrame* frame = aLine->mFirstChild;
if (aFloatAvailableSpace.mHasFloats) {
// There is a soft break opportunity at the start of the line, because
// we can always move this line down below float(s).
if (aLineLayout.NotifyOptionalBreakPosition(
frame, 0, true, gfxBreakPriority::eNormalBreak)) {
- lineReflowStatus = LINE_REFLOW_REDO_NEXT_BAND;
+ lineReflowStatus = LineReflowStatus::RedoNextBand;
}
}
// need to repeatedly call GetChildCount here, because the child
// count can change during the loop!
- for (i = 0; LINE_REFLOW_OK == lineReflowStatus && i < aLine->GetChildCount();
+ for (i = 0; LineReflowStatus::OK == lineReflowStatus && i < aLine->GetChildCount();
i++, frame = frame->GetNextSibling()) {
ReflowInlineFrame(aState, aLineLayout, aLine, frame, &lineReflowStatus);
- if (LINE_REFLOW_OK != lineReflowStatus) {
+ if (LineReflowStatus::OK != lineReflowStatus) {
// It is possible that one or more of next lines are empty
// (because of DeleteNextInFlowChild). If so, delete them now
// in case we are finished.
++aLine;
while ((aLine != end_lines()) && (0 == aLine->GetChildCount())) {
// XXX Is this still necessary now that DeleteNextInFlowChild
// uses DoRemoveFrame?
nsLineBox *toremove = aLine;
aLine = mLines.erase(aLine);
NS_ASSERTION(nullptr == toremove->mFirstChild, "bad empty line");
FreeLineBox(toremove);
}
--aLine;
- NS_ASSERTION(lineReflowStatus != LINE_REFLOW_TRUNCATED,
+ NS_ASSERTION(lineReflowStatus != LineReflowStatus::Truncated,
"ReflowInlineFrame should never determine that a line "
"needs to go to the next page/column");
}
}
// Don't pull up new frames into lines with continuation placeholders
if (aAllowPullUp) {
// Pull frames and reflow them until we can't
- while (LINE_REFLOW_OK == lineReflowStatus) {
+ while (LineReflowStatus::OK == lineReflowStatus) {
frame = PullFrame(aState, aLine);
if (!frame) {
break;
}
- while (LINE_REFLOW_OK == lineReflowStatus) {
+ while (LineReflowStatus::OK == lineReflowStatus) {
int32_t oldCount = aLine->GetChildCount();
ReflowInlineFrame(aState, aLineLayout, aLine, frame, &lineReflowStatus);
if (aLine->GetChildCount() != oldCount) {
// We just created a continuation for aFrame AND its going
// to end up on this line (e.g. :first-letter
// situation). Therefore we have to loop here before trying
// to pull another frame.
frame = frame->GetNextSibling();
@@ -3938,39 +3945,40 @@ nsBlockFrame::DoReflowInlineFrames(Block
}
}
}
aState.mFlags.mIsLineLayoutEmpty = aLineLayout.LineIsEmpty();
// We only need to backup if the line isn't going to be reflowed again anyway
bool needsBackup = aLineLayout.NeedsBackup() &&
- (lineReflowStatus == LINE_REFLOW_STOP || lineReflowStatus == LINE_REFLOW_OK);
+ (lineReflowStatus == LineReflowStatus::Stop ||
+ lineReflowStatus == LineReflowStatus::OK);
if (needsBackup && aLineLayout.HaveForcedBreakPosition()) {
NS_WARNING("We shouldn't be backing up more than once! "
"Someone must have set a break opportunity beyond the available width, "
"even though there were better break opportunities before it");
needsBackup = false;
}
if (needsBackup) {
// We need to try backing up to before a text run
// XXX It's possible, in fact not unusual, for the break opportunity to already
// be the end of the line. We should detect that and optimize to not
// re-do the line.
if (aLineLayout.HasOptionalBreakPosition()) {
// We can back up!
- lineReflowStatus = LINE_REFLOW_REDO_NO_PULL;
+ lineReflowStatus = LineReflowStatus::RedoNoPull;
}
} else {
// In case we reflow this line again, remember that we don't
// need to force any breaking
aLineLayout.ClearOptionalBreakPosition();
}
- if (LINE_REFLOW_REDO_NEXT_BAND == lineReflowStatus) {
+ if (LineReflowStatus::RedoNextBand == lineReflowStatus) {
// This happens only when we have a line that is impacted by
// floats and the first element in the line doesn't fit with
// the floats.
//
// What we do is to advance past the first float we find and
// then reflow the line all over again.
NS_ASSERTION(NS_UNCONSTRAINEDSIZE !=
aFloatAvailableSpace.mRect.BSize(outerWM),
@@ -3998,42 +4006,42 @@ nsBlockFrame::DoReflowInlineFrames(Block
// and needs to happen after the caller pops the space manager
// state.
aState.mFloatManager->AssertStateMatches(aFloatStateBeforeLine);
aFloatAvailableSpace = aState.GetFloatAvailableSpace();
} else {
// There's nowhere to retry placing the line, so we want to push
// it to the next page/column where its contents can fit not
// next to a float.
- lineReflowStatus = LINE_REFLOW_TRUNCATED;
+ lineReflowStatus = LineReflowStatus::Truncated;
PushTruncatedLine(aState, aLine, aKeepReflowGoing);
}
}
// XXX: a small optimization can be done here when paginating:
// if the new Y coordinate is past the end of the block then
// push the line and return now instead of later on after we are
// past the float.
}
- else if (LINE_REFLOW_TRUNCATED != lineReflowStatus &&
- LINE_REFLOW_REDO_NO_PULL != lineReflowStatus) {
+ else if (LineReflowStatus::Truncated != lineReflowStatus &&
+ LineReflowStatus::RedoNoPull != lineReflowStatus) {
// If we are propagating out a break-before status then there is
// no point in placing the line.
if (!NS_INLINE_IS_BREAK_BEFORE(aState.mReflowStatus)) {
if (!PlaceLine(aState, aLineLayout, aLine, aFloatStateBeforeLine,
aFloatAvailableSpace.mRect, aAvailableSpaceHeight,
aKeepReflowGoing)) {
- lineReflowStatus = LINE_REFLOW_REDO_MORE_FLOATS;
+ lineReflowStatus = LineReflowStatus::RedoMoreFloats;
// PlaceLine already called GetAvailableSpaceForBSize for us.
}
}
}
#ifdef DEBUG
if (gNoisyReflow) {
- printf("Line reflow status = %s\n", LineReflowStatusNames[lineReflowStatus]);
+ printf("Line reflow status = %s\n", LineReflowStatusToString(lineReflowStatus));
}
#endif
if (aLineLayout.GetDirtyNextLine()) {
// aLine may have been pushed to the overflow lines.
FrameLines* overflowLines = GetOverflowLines();
// We can't just compare iterators front() to aLine here, since they may be in
// different lists.
@@ -4070,18 +4078,18 @@ nsBlockFrame::ReflowInlineFrame(BlockRef
line_iterator aLine,
nsIFrame* aFrame,
LineReflowStatus* aLineReflowStatus)
{
if (!aFrame) { // XXX change to MOZ_ASSERT(aFrame)
NS_ERROR("why call me?");
return;
}
-
- *aLineReflowStatus = LINE_REFLOW_OK;
+
+ *aLineReflowStatus = LineReflowStatus::OK;
#ifdef NOISY_FIRST_LETTER
ListTag(stdout);
printf(": reflowing ");
nsFrame::ListTag(stdout, aFrame);
printf(" reflowingFirstLetter=%s\n",
aLineLayout.GetFirstLetterStyleOK() ? "on" : "off");
#endif
@@ -4119,31 +4127,31 @@ nsBlockFrame::ReflowInlineFrame(BlockRef
// break-after-not-complete. There are two situations: we are a
// block or we are an inline. This makes a total of 10 cases
// (fortunately, there is some overlap).
aLine->SetBreakTypeAfter(StyleClear::None);
if (NS_INLINE_IS_BREAK(frameReflowStatus) ||
StyleClear::None != aState.mFloatBreakType) {
// Always abort the line reflow (because a line break is the
// minimal amount of break we do).
- *aLineReflowStatus = LINE_REFLOW_STOP;
+ *aLineReflowStatus = LineReflowStatus::Stop;
// XXX what should aLine's break-type be set to in all these cases?
StyleClear breakType = NS_INLINE_GET_BREAK_TYPE(frameReflowStatus);
MOZ_ASSERT(StyleClear::None != breakType ||
StyleClear::None != aState.mFloatBreakType, "bad break type");
if (NS_INLINE_IS_BREAK_BEFORE(frameReflowStatus)) {
// Break-before cases.
if (aFrame == aLine->mFirstChild) {
// If we break before the first frame on the line then we must
// be trying to place content where there's no room (e.g. on a
// line with wide floats). Inform the caller to reflow the
// line after skipping past a float.
- *aLineReflowStatus = LINE_REFLOW_REDO_NEXT_BAND;
+ *aLineReflowStatus = LineReflowStatus::RedoNextBand;
}
else {
// It's not the first child on this line so go ahead and split
// the line. We will see the frame again on the next-line.
SplitLine(aState, aLineLayout, aLine, aFrame, aLineReflowStatus);
// If we're splitting the line because the frame didn't fit and it
// was pushed, then mark the line as having word wrapped. We need to
@@ -4185,25 +4193,25 @@ nsBlockFrame::ReflowInlineFrame(BlockRef
// Create a continuation for the incomplete frame. Note that the
// frame may already have a continuation.
CreateContinuationFor(aState, aLine, aFrame);
// Remember that the line has wrapped
if (!aLineLayout.GetLineEndsInBR()) {
aLine->SetLineWrapped(true);
}
-
- // If we just ended a first-letter frame or reflowed a placeholder then
+
+ // If we just ended a first-letter frame or reflowed a placeholder then
// don't split the line and don't stop the line reflow...
// But if we are going to stop anyways we'd better split the line.
- if ((!(frameReflowStatus & NS_INLINE_BREAK_FIRST_LETTER_COMPLETE) &&
+ if ((!(frameReflowStatus & NS_INLINE_BREAK_FIRST_LETTER_COMPLETE) &&
nsGkAtoms::placeholderFrame != aFrame->GetType()) ||
- *aLineReflowStatus == LINE_REFLOW_STOP) {
+ *aLineReflowStatus == LineReflowStatus::Stop) {
// Split line after the current frame
- *aLineReflowStatus = LINE_REFLOW_STOP;
+ *aLineReflowStatus = LineReflowStatus::Stop;
SplitLine(aState, aLineLayout, aLine, aFrame->GetNextSibling(), aLineReflowStatus);
}
}
}
bool
nsBlockFrame::CreateContinuationFor(BlockReflowInput& aState,
nsLineBox* aLine,
@@ -4353,22 +4361,22 @@ nsBlockFrame::SplitLine(BlockReflowInput
#endif
// Let line layout know that some frames are no longer part of its
// state.
aLineLayout.SplitLineTo(aLine->GetChildCount());
// If floats have been placed whose placeholders have been pushed to the new
// line, we need to reflow the old line again. We don't want to look at the
- // frames in the new line, because as a large paragraph is laid out the
+ // frames in the new line, because as a large paragraph is laid out the
// we'd get O(N^2) performance. So instead we just check that the last
// float and the last below-current-line float are still in aLine.
if (!CheckPlaceholderInLine(this, aLine, GetLastFloat(aLine)) ||
!CheckPlaceholderInLine(this, aLine, aState.mBelowCurrentLineFloats.Tail())) {
- *aLineReflowStatus = LINE_REFLOW_REDO_NO_PULL;
+ *aLineReflowStatus = LineReflowStatus::RedoNoPull;
}
#ifdef DEBUG
VerifyLines(true);
#endif
}
}
@@ -4458,17 +4466,17 @@ nsBlockFrame::PlaceLine(BlockReflowInput
aAvailableSpaceHeight,
aFloatStateBeforeLine).mRect;
NS_ASSERTION(aFloatAvailableSpace.BStart(wm) ==
oldFloatAvailableSpace.BStart(wm), "yikes");
// Restore the height to the position of the next band.
aFloatAvailableSpace.BSize(wm) = oldFloatAvailableSpace.BSize(wm);
// If the available space between the floats is smaller now that we
// know the height, return false (and cause another pass with
- // LINE_REFLOW_REDO_MORE_FLOATS). We ensure aAvailableSpaceHeight
+ // LineReflowStatus::RedoMoreFloats). We ensure aAvailableSpaceHeight
// never decreases, which means that we can't reduce the set of floats
// we intersect, which means that the available space cannot grow.
if (AvailableSpaceShrunk(wm, oldFloatAvailableSpace, aFloatAvailableSpace,
false)) {
return false;
}
#ifdef DEBUG
--- a/layout/generic/nsBlockFrame.h
+++ b/layout/generic/nsBlockFrame.h
@@ -13,36 +13,36 @@
#define nsBlockFrame_h___
#include "nsContainerFrame.h"
#include "nsHTMLParts.h"
#include "nsLineBox.h"
#include "nsCSSPseudoElements.h"
#include "nsFloatManager.h"
-enum LineReflowStatus {
+enum class LineReflowStatus {
// The line was completely reflowed and fit in available width, and we should
// try to pull up content from the next line if possible.
- LINE_REFLOW_OK,
+ OK,
// The line was completely reflowed and fit in available width, but we should
// not try to pull up content from the next line.
- LINE_REFLOW_STOP,
+ Stop,
// We need to reflow the line again at its current vertical position. The
// new reflow should not try to pull up any frames from the next line.
- LINE_REFLOW_REDO_NO_PULL,
+ RedoNoPull,
// We need to reflow the line again using the floats from its height
// this reflow, since its height made it hit floats that were not
// adjacent to its top.
- LINE_REFLOW_REDO_MORE_FLOATS,
+ RedoMoreFloats,
// We need to reflow the line again at a lower vertical postion where there
// may be more horizontal space due to different float configuration.
- LINE_REFLOW_REDO_NEXT_BAND,
+ RedoNextBand,
// The line did not fit in the available vertical space. Try pushing it to
// the next page or column if it's not the first line on the current page/column.
- LINE_REFLOW_TRUNCATED
+ Truncated
};
class nsBlockInFlowLineIterator;
class nsBulletFrame;
namespace mozilla {
class BlockReflowInput;
} // namespace mozilla
@@ -141,16 +141,17 @@ public:
#ifdef DEBUG_FRAME_DUMP
void List(FILE* out = stderr, const char* aPrefix = "", uint32_t aFlags = 0) const override;
virtual nsresult GetFrameName(nsAString& aResult) const override;
#endif
#ifdef DEBUG
virtual nsFrameState GetDebugStateBits() const override;
+ const char* LineReflowStatusToString(LineReflowStatus aLineReflowStatus) const;
#endif
#ifdef ACCESSIBILITY
virtual mozilla::a11y::AccType AccessibleType() override;
#endif
// line cursor methods to speed up searching for the line(s)
// containing a point. The basic idea is that we set the cursor
--- a/layout/generic/nsLineBox.cpp
+++ b/layout/generic/nsLineBox.cpp
@@ -197,18 +197,17 @@ nsLineBox::BreakTypeToString(StyleClear
switch (aBreakType) {
case StyleClear::None: return "nobr";
case StyleClear::Left: return "leftbr";
case StyleClear::Right: return "rightbr";
case StyleClear::InlineStart: return "inlinestartbr";
case StyleClear::InlineEnd: return "inlineendbr";
case StyleClear::Both: return "leftbr+rightbr";
case StyleClear::Line: return "linebr";
- default:
- break;
+ case StyleClear::Max: return "leftbr+rightbr+linebr";
}
return "unknown";
}
char*
nsLineBox::StateToString(char* aBuf, int32_t aBufSize) const
{
snprintf(aBuf, aBufSize, "%s,%s,%s,%s,%s,before:%s,after:%s[0x%x]",
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
@@ -23,16 +23,17 @@
#include "mozilla/Telemetry.h"
#endif
#include "webrtc/common.h"
#include "webrtc/modules/audio_processing/include/audio_processing.h"
#include "webrtc/modules/rtp_rtcp/interface/rtp_rtcp.h"
#include "webrtc/voice_engine/include/voe_dtmf.h"
#include "webrtc/voice_engine/include/voe_errors.h"
+#include "webrtc/voice_engine/voice_engine_impl.h"
#include "webrtc/system_wrappers/interface/clock.h"
#ifdef MOZ_WIDGET_ANDROID
#include "AndroidJNIWrapper.h"
#endif
namespace mozilla {
@@ -234,16 +235,30 @@ bool WebrtcAudioConduit::SetDtmfPayloadT
int result = mPtrVoEDtmf->SetSendTelephoneEventPayloadType(mChannel, type);
if (result == -1) {
CSFLogError(logTag, "%s Failed call to SetSendTelephoneEventPayloadType",
__FUNCTION__);
}
return result != -1;
}
+bool WebrtcAudioConduit::InsertDTMFTone(int channel, int eventCode,
+ bool outOfBand, int lengthMs,
+ int attenuationDb) {
+ NS_ASSERTION(!NS_IsMainThread(), "Do not call on main thread");
+
+ if (!mVoiceEngine || !mDtmfEnabled) {
+ return false;
+ }
+
+ webrtc::VoiceEngineImpl* s = static_cast<webrtc::VoiceEngineImpl*>(mVoiceEngine);
+ int result = s->SendTelephoneEvent(channel, eventCode, outOfBand, lengthMs, attenuationDb);
+ return result != -1;
+}
+
/*
* WebRTCAudioConduit Implementation
*/
MediaConduitErrorCode WebrtcAudioConduit::Init()
{
CSFLogDebug(logTag, "%s this=%p", __FUNCTION__, this);
#ifdef MOZ_WIDGET_ANDROID
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.h
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.h
@@ -217,16 +217,19 @@ public:
uint32_t *cumulativeLost,
int32_t* rttMs) override;
bool GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
unsigned int* packetsSent,
uint64_t* bytesSent) override;
bool SetDtmfPayloadType(unsigned char type) override;
+ bool InsertDTMFTone(int channel, int eventCode, bool outOfBand,
+ int lengthMs, int attenuationDb) override;
+
private:
WebrtcAudioConduit(const WebrtcAudioConduit& other) = delete;
void operator=(const WebrtcAudioConduit& other) = delete;
//Local database of currently applied receive codecs
typedef std::vector<AudioCodecConfig* > RecvCodecList;
//Function to convert between WebRTC and Conduit codec structures
--- a/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
+++ b/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
@@ -482,11 +482,14 @@ public:
* @param enabled: enable extension
* @param id: id to be used for this rtp header extension
* NOTE: See AudioConduit for more information
*/
virtual MediaConduitErrorCode EnableAudioLevelExtension(bool enabled, uint8_t id) = 0;
virtual bool SetDtmfPayloadType(unsigned char type) = 0;
+ virtual bool InsertDTMFTone(int channel, int eventCode, bool outOfBand,
+ int lengthMs, int attenuationDb) = 0;
+
};
}
#endif
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -74,16 +74,18 @@
#include "nsNetUtil.h"
#include "nsIURLParser.h"
#include "nsIDOMDataChannel.h"
#include "nsIDOMLocation.h"
#include "nsNullPrincipal.h"
#include "mozilla/PeerIdentity.h"
#include "mozilla/dom/RTCCertificate.h"
#include "mozilla/dom/RTCConfigurationBinding.h"
+#include "mozilla/dom/RTCDTMFSenderBinding.h"
+#include "mozilla/dom/RTCDTMFToneChangeEvent.h"
#include "mozilla/dom/RTCRtpSenderBinding.h"
#include "mozilla/dom/RTCStatsReportBinding.h"
#include "mozilla/dom/RTCPeerConnectionBinding.h"
#include "mozilla/dom/PeerConnectionImplBinding.h"
#include "mozilla/dom/DataChannelBinding.h"
#include "mozilla/dom/PerformanceTiming.h"
#include "mozilla/dom/PluginCrashedEvent.h"
#include "MediaStreamList.h"
@@ -133,17 +135,16 @@
using namespace mozilla;
using namespace mozilla::dom;
typedef PCObserverString ObString;
static const char* logTag = "PeerConnectionImpl";
-
// Getting exceptions back down from PCObserver is generally not harmful.
namespace {
// This is a terrible hack. The problem is that SuppressException is not
// inline, and we link this file without libxul in some cases (e.g. for our test
// setup). So we can't use ErrorResult or IgnoredErrorResult because those call
// SuppressException... And we can't use FastErrorResult because we can't
// include BindingUtils.h, because our linking is completely fucked up. Use
// BaseErrorResult directly. Please do not let me see _anyone_ doing this
@@ -2493,16 +2494,29 @@ PeerConnectionImpl::SelectSsrc(MediaStre
return NS_OK;
}
NS_IMETHODIMP
PeerConnectionImpl::RemoveTrack(MediaStreamTrack& aTrack) {
PC_AUTO_ENTER_API_CALL(true);
std::string trackId = PeerConnectionImpl::GetTrackId(aTrack);
+
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+ nsString wideTrackId;
+ aTrack.GetId(wideTrackId);
+ for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
+ if (mDTMFStates[i].mTrackId == wideTrackId) {
+ mDTMFStates[i].mSendTimer->Cancel();
+ mDTMFStates.RemoveElementAt(i);
+ break;
+ }
+ }
+#endif
+
RefPtr<LocalSourceStreamInfo> info = media()->GetLocalStreamByTrackId(trackId);
if (!info) {
CSFLogError(logTag, "%s: Unknown stream", __FUNCTION__);
return NS_ERROR_INVALID_ARG;
}
nsresult rv =
@@ -2520,21 +2534,140 @@ PeerConnectionImpl::RemoveTrack(MediaStr
aTrack.RemovePrincipalChangeObserver(this);
OnNegotiationNeeded();
return NS_OK;
}
+static int GetDTMFToneCode(uint16_t c)
+{
+ const char* DTMF_TONECODES = "0123456789*#ABCD";
+
+ if (c == ',') {
+ // , is a special character indicating a 2 second delay
+ return -1;
+ }
+
+ const char* i = strchr(DTMF_TONECODES, c);
+ MOZ_ASSERT(i);
+ return i - DTMF_TONECODES;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::InsertDTMF(mozilla::dom::RTCRtpSender& sender,
+ const nsAString& tones, uint32_t duration,
+ uint32_t interToneGap) {
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+ PC_AUTO_ENTER_API_CALL(false);
+
+ JSErrorResult jrv;
+
+ // Retrieve track
+ RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
+ if (jrv.Failed()) {
+ NS_WARNING("Failed to retrieve track for RTCRtpSender!");
+ return jrv.StealNSResult();
+ }
+
+ nsString senderTrackId;
+ mst->GetId(senderTrackId);
+
+ // Attempt to locate state for the DTMFSender
+ DTMFState* state = nullptr;
+ for (auto& dtmfState : mDTMFStates) {
+ if (dtmfState.mTrackId == senderTrackId) {
+ state = &dtmfState;
+ break;
+ }
+ }
+
+ // No state yet, create a new one
+ if (!state) {
+ state = mDTMFStates.AppendElement();
+ state->mPeerConnectionImpl = this;
+ state->mTrackId = senderTrackId;
+ state->mSendTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ MOZ_ASSERT(state->mSendTimer);
+ }
+ MOZ_ASSERT(state);
+
+ auto trackPairs = mJsepSession->GetNegotiatedTrackPairs();
+ state->mLevel = -1;
+ for (auto& trackPair : trackPairs) {
+ if (state->mTrackId.EqualsASCII(trackPair.mSending->GetTrackId().c_str())) {
+ if (trackPair.mBundleLevel.isSome()) {
+ state->mLevel = *trackPair.mBundleLevel;
+ } else {
+ state->mLevel = trackPair.mLevel;
+ }
+ break;
+ }
+ }
+
+ state->mTones = tones;
+ state->mDuration = duration;
+ state->mInterToneGap = interToneGap;
+ if (!state->mTones.IsEmpty()) {
+ state->mSendTimer->InitWithFuncCallback(DTMFSendTimerCallback_m, state, 0,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::GetDTMFToneBuffer(mozilla::dom::RTCRtpSender& sender,
+ nsAString& outToneBuffer) {
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+ PC_AUTO_ENTER_API_CALL(false);
+
+ JSErrorResult jrv;
+
+ // Retrieve track
+ RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
+ if (jrv.Failed()) {
+ NS_WARNING("Failed to retrieve track for RTCRtpSender!");
+ return jrv.StealNSResult();
+ }
+
+ nsString senderTrackId;
+ mst->GetId(senderTrackId);
+
+ // Attempt to locate state for the DTMFSender
+ for (auto& dtmfState : mDTMFStates) {
+ if (dtmfState.mTrackId == senderTrackId) {
+ outToneBuffer = dtmfState.mTones;
+ break;
+ }
+ }
+#endif
+
+ return NS_OK;
+}
+
NS_IMETHODIMP
PeerConnectionImpl::ReplaceTrack(MediaStreamTrack& aThisTrack,
MediaStreamTrack& aWithTrack) {
PC_AUTO_ENTER_API_CALL(true);
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+ nsString trackId;
+ aThisTrack.GetId(trackId);
+
+ for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
+ if (mDTMFStates[i].mTrackId == trackId) {
+ mDTMFStates[i].mSendTimer->Cancel();
+ mDTMFStates.RemoveElementAt(i);
+ break;
+ }
+ }
+#endif
+
RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
if (!pco) {
return NS_ERROR_UNEXPECTED;
}
JSErrorResult jrv;
#if !defined(MOZILLA_EXTERNAL_LINKAGE)
if (&aThisTrack == &aWithTrack) {
@@ -2941,16 +3074,20 @@ PeerConnectionImpl::RecordEndOfCallTelem
#endif
}
nsresult
PeerConnectionImpl::CloseInt()
{
PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ for (auto& dtmfState : mDTMFStates) {
+ dtmfState.mSendTimer->Cancel();
+ }
+
// We do this at the end of the call because we want to make sure we've waited
// for all trickle ICE candidates to come in; this can happen well after we've
// transitioned to connected. As a bonus, this allows us to detect race
// conditions where a stats dispatch happens right as the PC closes.
if (!mPrivateWindow) {
RecordLongtermICEStatistics();
}
RecordEndOfCallTelemetry();
@@ -3983,9 +4120,68 @@ PeerConnectionImpl::GetRemoteStreams(nsT
result.AppendElement(info->GetMediaStream());
}
return NS_OK;
#else
return NS_ERROR_FAILURE;
#endif
}
+void
+PeerConnectionImpl::DTMFSendTimerCallback_m(nsITimer* timer, void* closure)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto state = static_cast<DTMFState*>(closure);
+
+ nsString eventTone;
+ if (!state->mTones.IsEmpty()) {
+ uint16_t toneChar = state->mTones.CharAt(0);
+ int tone = GetDTMFToneCode(toneChar);
+
+ eventTone.Assign(toneChar);
+
+ state->mTones.Cut(0, 1);
+
+ if (tone == -1) {
+ state->mSendTimer->InitWithFuncCallback(DTMFSendTimerCallback_m, state,
+ 2000, nsITimer::TYPE_ONE_SHOT);
+ } else {
+ // Reset delay if necessary
+ state->mSendTimer->InitWithFuncCallback(DTMFSendTimerCallback_m, state,
+ state->mDuration + state->mInterToneGap,
+ nsITimer::TYPE_ONE_SHOT);
+
+ RefPtr<AudioSessionConduit> conduit =
+ state->mPeerConnectionImpl->mMedia->GetAudioConduit(state->mLevel);
+
+ if (conduit) {
+ uint32_t duration = state->mDuration;
+ state->mPeerConnectionImpl->mSTSThread->Dispatch(WrapRunnableNM([conduit, tone, duration] () {
+ //Note: We default to channel 0, not inband, and 6dB attenuation.
+ // here. We might want to revisit these choices in the future.
+ conduit->InsertDTMFTone(0, tone, true, duration, 6);
+ }), NS_DISPATCH_NORMAL);
+ }
+
+ }
+ } else {
+ state->mSendTimer->Cancel();
+ }
+
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+ RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(state->mPeerConnectionImpl->mPCObserver);
+ if (!pco) {
+ NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
+ return;
+ }
+
+ JSErrorResult jrv;
+ pco->OnDTMFToneChange(state->mTrackId, eventTone, jrv);
+
+ if (jrv.Failed()) {
+ NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
+ return;
+ }
+#endif
+}
+
} // end mozilla namespace
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -71,19 +71,21 @@ class MediaPipeline;
typedef Fake_DOMMediaStream DOMMediaStream;
#else
class DOMMediaStream;
#endif
namespace dom {
class RTCCertificate;
struct RTCConfiguration;
+class RTCDTMFSender;
struct RTCIceServer;
struct RTCOfferOptions;
struct RTCRtpParameters;
+class RTCRtpSender;
#ifdef USE_FAKE_MEDIA_STREAMS
typedef Fake_MediaStreamTrack MediaStreamTrack;
#else
class MediaStreamTrack;
#endif
#ifdef USE_FAKE_PCOBSERVER
typedef test::AFakePCObserver PeerConnectionObserver;
@@ -431,16 +433,29 @@ public:
mozilla::dom::MediaStreamTrack& aTrack)
{
rv = RemoveTrack(aTrack);
}
nsresult
AddTrack(mozilla::dom::MediaStreamTrack& aTrack, DOMMediaStream& aStream);
+ NS_IMETHODIMP_TO_ERRORRESULT(InsertDTMF, ErrorResult &rv,
+ dom::RTCRtpSender& sender,
+ const nsAString& tones,
+ uint32_t duration, uint32_t interToneGap) {
+ rv = InsertDTMF(sender, tones, duration, interToneGap);
+ }
+
+ NS_IMETHODIMP_TO_ERRORRESULT(GetDTMFToneBuffer, ErrorResult &rv,
+ dom::RTCRtpSender& sender,
+ nsAString& outToneBuffer) {
+ rv = GetDTMFToneBuffer(sender, outToneBuffer);
+ }
+
NS_IMETHODIMP_TO_ERRORRESULT(ReplaceTrack, ErrorResult &rv,
mozilla::dom::MediaStreamTrack& aThisTrack,
mozilla::dom::MediaStreamTrack& aWithTrack)
{
rv = ReplaceTrack(aThisTrack, aWithTrack);
}
#if !defined(MOZILLA_EXTERNAL_LINKAGE)
@@ -832,16 +847,32 @@ private:
bool mNegotiationNeeded;
bool mPrivateWindow;
// storage for Telemetry data
uint16_t mMaxReceiving[SdpMediaSection::kMediaTypes];
uint16_t mMaxSending[SdpMediaSection::kMediaTypes];
+ // DTMF
+ struct DTMFState {
+ PeerConnectionImpl* mPeerConnectionImpl;
+ nsCOMPtr<nsITimer> mSendTimer;
+ nsString mTrackId;
+ nsString mTones;
+ size_t mLevel;
+ uint32_t mDuration;
+ uint32_t mInterToneGap;
+ };
+
+ static void
+ DTMFSendTimerCallback_m(nsITimer* timer, void*);
+
+ nsTArray<DTMFState> mDTMFStates;
+
public:
//these are temporary until the DataChannel Listen/Connect API is removed
unsigned short listenPort;
unsigned short connectPort;
char *connectStr; // XXX ownership/free
};
// This is what is returned when you acquire on a handle
--- a/media/webrtc/trunk/webrtc/voice_engine/dtmf_inband.cc
+++ b/media/webrtc/trunk/webrtc/voice_engine/dtmf_inband.cc
@@ -29,16 +29,26 @@ const int16_t Dtmf_a_times2Tab16Khz[8]=
29144, 28361, 27409, 26258
};
const int16_t Dtmf_a_times2Tab32Khz[8]=
{
32462,32394, 32311, 32210, 31849, 31647, 31400, 31098
};
+const int16_t Dtmf_a_times2Tab44_1Khz[8]=
+{
+ 32607, 32571, 32527, 32474, 32283, 32176, 32045, 31885
+};
+
+const int16_t Dtmf_a_times2Tab48Khz[8]=
+{
+ 32612, 32577, 32534, 32483, 32298, 32194, 32067, 31912
+};
+
// Second table is sin(2*pi*f/fs) in Q14
const int16_t Dtmf_ym2Tab8Khz[8]=
{
8527, 9315, 10163, 11036,
13322, 14206, 15021, 15708
};
@@ -48,16 +58,26 @@ const int16_t Dtmf_ym2Tab16Khz[8]=
7490, 8207, 8979, 9801
};
const int16_t Dtmf_ym2Tab32Khz[8]=
{
2235, 2468, 2728, 3010, 3853, 4249, 4685, 5164
};
+const int16_t Dtmf_ym2Tab44_1Khz[8]=
+{
+ 1624, 1794, 1984, 2190, 2808, 3100, 3422, 3777
+};
+
+const int16_t Dtmf_ym2Tab48Khz[8]=
+{
+ 1599, 1766, 1953, 2156, 2765, 3052, 3369, 3719
+};
+
const int16_t Dtmf_dBm0kHz[37]=
{
16141, 14386, 12821, 11427, 10184, 9077,
8090, 7210, 6426, 5727, 5104, 4549,
4054, 3614, 3221, 2870, 2558, 2280,
2032, 1811, 1614, 1439, 1282, 1143,
1018, 908, 809, 721, 643, 573,
510, 455, 405, 361, 322, 287,
@@ -87,17 +107,19 @@ DtmfInband::~DtmfInband()
delete &_critSect;
}
int
DtmfInband::SetSampleRate(uint16_t frequency)
{
if (frequency != 8000 &&
frequency != 16000 &&
- frequency != 32000)
+ frequency != 32000 &&
+ frequency != 44100 &&
+ frequency != 48000)
{
// invalid sample rate
assert(false);
return -1;
}
_outputFrequencyHz = frequency;
return 0;
}
@@ -277,16 +299,22 @@ DtmfInband::DtmfFix_generate(int16_t *de
a_times2Tbl=Dtmf_a_times2Tab8Khz;
y2_Table=Dtmf_ym2Tab8Khz;
} else if (fs==16000) {
a_times2Tbl=Dtmf_a_times2Tab16Khz;
y2_Table=Dtmf_ym2Tab16Khz;
} else if (fs==32000) {
a_times2Tbl=Dtmf_a_times2Tab32Khz;
y2_Table=Dtmf_ym2Tab32Khz;
+ } else if (fs==44100) {
+ a_times2Tbl=Dtmf_a_times2Tab44_1Khz;
+ y2_Table=Dtmf_ym2Tab44_1Khz;
+ } else if (fs==48000) {
+ a_times2Tbl=Dtmf_a_times2Tab48Khz;
+ y2_Table=Dtmf_ym2Tab48Khz;
} else {
return(-1);
}
if ((value==1)||(value==2)||(value==3)||(value==12)) {
a1_times2=a_times2Tbl[0];
if (_reinit) {
_oldOutputLow[0]=y2_Table[0];
--- a/media/webrtc/trunk/webrtc/voice_engine/output_mixer.cc
+++ b/media/webrtc/trunk/webrtc/voice_engine/output_mixer.cc
@@ -594,16 +594,32 @@ void OutputMixer::APMAnalyzeReverseStrea
// Private methods
// ----------------------------------------------------------------------------
int
OutputMixer::InsertInbandDtmfTone()
{
uint16_t sampleRate(0);
_dtmfGenerator.GetSampleRate(sampleRate);
+
+ // We're not using a supported sample rate for the DtmfInband generator, so
+ // we won't be able to generate feedback tones.
+ if (!(_audioFrame.sample_rate_hz_ == 8000 ||
+ _audioFrame.sample_rate_hz_ == 16000 ||
+ _audioFrame.sample_rate_hz_ == 32000 ||
+ _audioFrame.sample_rate_hz_ == 44100 ||
+ _audioFrame.sample_rate_hz_ == 48000)) {
+
+ WEBRTC_TRACE(kTraceError, kTraceVoice, VoEId(_instanceId, -1),
+ "OutputMixer::InsertInbandDtmfTone() Sample rate"
+ "not supported");
+
+ return -1;
+ }
+
if (sampleRate != _audioFrame.sample_rate_hz_)
{
// Update sample rate of Dtmf tone since the mixing frequency changed.
_dtmfGenerator.SetSampleRate(
(uint16_t)(_audioFrame.sample_rate_hz_));
// Reset the tone to be added taking the new sample rate into account.
_dtmfGenerator.ResetTone();
}
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -84,17 +84,16 @@ android {
main {
manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
aidl {
srcDir "${topsrcdir}/mobile/android/base/aidl"
}
java {
- srcDir "${topsrcdir}/mobile/android/geckoview/src/main/java"
srcDir "${topsrcdir}/mobile/android/base/java"
srcDir "${topsrcdir}/mobile/android/search/java"
srcDir "${topsrcdir}/mobile/android/javaaddons/java"
srcDir "${topsrcdir}/mobile/android/services/src/main/java"
if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
srcDir "${topsrcdir}/mobile/android/stumbler/java"
}
@@ -225,16 +224,17 @@ dependencies {
// of this library.
// It doesn't seem like there is a non-trivial way to be conditional on 'localOld', so instead we explicitly
// define a version of leakcanary for every flavor:
localCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
localOldCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
automationCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
+ compile project(':geckoview')
compile project(':thirdparty')
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.1.2'
testCompile 'org.simpleframework:simple-http:6.0.1'
testCompile 'org.mockito:mockito-core:1.10.19'
// Including the Robotium JAR directly can cause issues with dexing.
@@ -248,50 +248,22 @@ task checkstyle(type: Checkstyle) {
// TODO: should use sourceSets from project instead of hard-coded str.
source '../base/java/'
// TODO: This ignores our pre-processed resources.
include '**/*.java'
// TODO: classpath should probably be something.
classpath = files()
}
-task syncOmnijarFromDistDir(type: Sync) {
- into("${project.buildDir}/generated/omnijar")
- from("${topobjdir}/dist/fennec/assets") {
- include 'omni.ja'
- }
-}
-
-task checkLibsExistInDistDir<< {
- if (syncLibsFromDistDir.source.empty) {
- throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib. Have you built and packaged?")
- }
-}
-
-task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
- into("${project.buildDir}/generated/jniLibs")
- from("${topobjdir}/dist/fennec/lib")
-}
-
-task checkAssetsExistInDistDir<< {
- if (syncAssetsFromDistDir.source.empty) {
- throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets. Have you built and packaged?")
- }
-}
-
-task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
- into("${project.buildDir}/generated/assets")
- from("${topobjdir}/dist/fennec/assets") {
- exclude 'omni.ja'
- }
-}
-
task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
into("${project.buildDir}/generated/source/preprocessed_code")
- from("${topobjdir}/mobile/android/base/generated/preprocessed")
+ from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+ // All other preprocessed code is included in the geckoview project.
+ include '**/AdjustConstants.java'
+ }
}
// The localization system uses the moz.build preprocessor to interpolate a .dtd
// file of XML entity definitions into an XML file of elements referencing those
// entities. (Each locale produces its own .dtd file, backstopped by the en-US
// .dtd file in tree.) Android Studio (and IntelliJ) don't handle these inline
// entities smoothly. This filter merely expands the entities in place, making
// them appear properly throughout the IDE. Be aware that this assumes that the
@@ -307,82 +279,48 @@ class ExpandXMLEntitiesFilter extends Fi
task syncPreprocessedResources(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
into("${project.buildDir}/generated/source/preprocessed_resources")
from("${topobjdir}/mobile/android/base/res")
filesMatching('**/strings.xml') {
filter(ExpandXMLEntitiesFilter)
}
}
-// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
-// That arrangement labels them nicely in IntelliJ. See the comment in the
-// :omnijar project for more context.
-evaluationDependsOn(':omnijar')
-
-task buildOmnijar(type:Exec) {
- dependsOn rootProject.generateCodeAndResources
-
- // See comment in :omnijar project regarding interface mismatches here.
- inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
-
- // Produce a single output file.
- outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
-
- workingDir "${topobjdir}"
-
- commandLine mozconfig.substs.GMAKE
- args '-C'
- args "${topobjdir}/mobile/android/base"
- args 'gradle-omnijar'
-
- // Only show the output if something went wrong.
- ignoreExitValue = true
- standardOutput = new ByteArrayOutputStream()
- errorOutput = standardOutput
- doLast {
- if (execResult.exitValue != 0) {
- throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
- }
- }
-}
-
// It's not easy -- see the backout in Bug 1242213 -- to change the <manifest>
// package for Fennec. Gradle has grown a mechanism to achieve what we want for
// Fennec, however, with applicationId. To use the same manifest as moz.build,
// we replace the package with org.mozilla.gecko (the eventual package) here.
task rewriteManifestPackage(type: Copy, dependsOn: rootProject.generateCodeAndResources) {
into("${project.buildDir}/generated/source/preprocessed_manifest")
from("${topobjdir}/mobile/android/base/AndroidManifest.xml")
filter { it.replaceFirst(/package=".*?"/, 'package="org.mozilla.gecko"') }
}
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
android.applicationVariants.all { variant ->
variant.preBuild.dependsOn rewriteManifestPackage
variant.preBuild.dependsOn syncPreprocessedCode
variant.preBuild.dependsOn syncPreprocessedResources
+ // Automation builds don't include Gecko binaries, since those binaries are
+ // not produced until after build time (at package time). Therefore,
+ // automation builds include the Gecko binaries into the APK at package
+ // time. The "withGeckoBinaries" variant of the :geckoview project also
+ // does this. (It does what it says on the tin!) For notes on this
+ // approach, see mobile/android/gradle/with_gecko_binaries.gradle.
+
// Like 'local' or 'localOld'.
def productFlavor = variant.productFlavors[0].name
- // Like 'debug' or 'release'.
- def buildType = variant.buildType.name
- // We insert omni.ja and the .so libraries into all local builds.
- if (!productFlavor.startsWith('local')) {
- return
+ // :app uses :geckoview:release and handles it's own Gecko binary inclusion,
+ // even though this would be most naturally done in the :geckoview project.
+ if (!productFlavor.equals('automation')) {
+ configureVariantWithGeckoBinaries(variant)
}
-
- syncOmnijarFromDistDir.dependsOn buildOmnijar
- def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
- generateAssetsTask.dependsOn syncOmnijarFromDistDir
- generateAssetsTask.dependsOn syncLibsFromDistDir
- generateAssetsTask.dependsOn syncAssetsFromDistDir
-
- android.sourceSets."${productFlavor}${buildType.capitalize()}".assets.srcDir syncOmnijarFromDistDir.destinationDir
- android.sourceSets."${productFlavor}${buildType.capitalize()}".assets.srcDir syncAssetsFromDistDir.destinationDir
- android.sourceSets."${productFlavor}${buildType.capitalize()}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
}
apply plugin: 'spoon'
spoon {
// For now, let's be verbose.
debug = true
// It's not helpful to pass when we don't have a device connected.
@@ -421,37 +359,37 @@ afterEvaluate {
// Bug 1299015: Complain to treeherder if checkstyle, lint, or unittest fails. It's not obvious
// how to listen to individual errors in most cases, so we just link to the reports for now.
def makeTaskExecutionListener(artifactRootUrl) {
return new TaskExecutionListener() {
void beforeExecute(Task task) {
// Do nothing.
}
-
+
void afterExecute(Task task, TaskState state) {
if (!state.failure) {
return
}
-
+
// Link to the failing report. The task path and the report path
// depend on the android-lint task in
// taskcluster/ci/android-stuff/kind.yml. It's not possible to link
// directly, so for now consumers will need to copy-paste the URL.
switch (task.path) {
case ':app:checkstyle':
def url = "${artifactRootUrl}/public/android/checkstyle/checkstyle.xml"
println "TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: $url"
break
-
+
case ':app:lintAutomationDebug':
def url = "${artifactRootUrl}/public/android/lint/lint-results-automationDebug.html"
println "TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: $url"
break
-
+
case ':app:testAutomationDebugUnitTest':
def url = "${artifactRootUrl}/public/android/unittest/automationDebug/index.html"
println "TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the report at: $url"
break
}
}
}
}
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -28,17 +28,17 @@ import java.util.Timer;
import java.util.TimerTask;
import android.util.Log;
class ActionBarTextSelection implements TextSelection, GeckoEventListener {
private static final String LOGTAG = "GeckoTextSelection";
private static final int SHUTDOWN_DELAY_MS = 250;
- private final TextSelectionHandle anchorHandle;
+ private final Context context;
private boolean mDraggingHandles;
private String selectionID; // Unique ID provided for each selection action.
private String mCurrentItems;
private TextSelectionActionModeCallback mCallback;
@@ -54,25 +54,25 @@ class ActionBarTextSelection implements
public void run() {
endActionMode();
}
});
}
};
private ActionModeTimerTask mActionModeTimerTask;
- ActionBarTextSelection(TextSelectionHandle anchorHandle) {
- this.anchorHandle = anchorHandle;
+ ActionBarTextSelection(Context context) {
+ this.context = context;
}
@Override
public void create() {
// Only register listeners if we have valid start/middle/end handles
- if (anchorHandle == null) {
- Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
+ if (context == null) {
+ Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null");
} else {
GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
"TextSelection:ActionbarInit",
"TextSelection:ActionbarStatus",
"TextSelection:ActionbarUninit",
"TextSelection:Update");
}
}
@@ -80,18 +80,18 @@ class ActionBarTextSelection implements
@Override
public boolean dismiss() {
// We do not call endActionMode() here because this is already handled by the activity.
return false;
}
@Override
public void destroy() {
- if (anchorHandle == null) {
- Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since anchorHandle is null");
+ if (context == null) {
+ Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null");
} else {
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
"TextSelection:ActionbarInit",
"TextSelection:ActionbarStatus",
"TextSelection:ActionbarUninit",
"TextSelection:Update");
}
}
@@ -149,27 +149,25 @@ class ActionBarTextSelection implements
}
mCurrentItems = itemsString;
if (mCallback != null) {
mCallback.updateItems(items);
return;
}
- final Context context = anchorHandle.getContext();
if (context instanceof ActionModeCompat.Presenter) {
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
mCallback = new TextSelectionActionModeCallback(items);
presenter.startActionModeCompat(mCallback);
mCallback.animateIn();
}
}
private void endActionMode() {
- Context context = anchorHandle.getContext();
if (context instanceof ActionModeCompat.Presenter) {
final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
presenter.endActionModeCompat();
}
mCurrentItems = null;
}
private class TextSelectionActionModeCallback implements Callback {
@@ -205,17 +203,17 @@ class ActionBarTextSelection implements
for (int i = 0; i < length; i++) {
try {
final JSONObject obj = mItems.getJSONObject(i);
final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
final String iconString = obj.optString("icon");
- BitmapUtils.getDrawable(anchorHandle.getContext(), iconString, new BitmapLoader() {
+ BitmapUtils.getDrawable(context, iconString, new BitmapLoader() {
@Override
public void onBitmapFound(Drawable d) {
if (d != null) {
menuitem.setIcon(d);
}
}
});
} catch (Exception ex) {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1249,18 +1249,17 @@ public abstract class GeckoApp
mLayerView = (GeckoView) findViewById(R.id.layer_view);
Tabs.getInstance().attachToContext(this, mLayerView);
// Use global layout state change to kick off additional initialization
mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
if (Versions.preMarshmallow) {
- mTextSelection = new ActionBarTextSelection(
- (TextSelectionHandle) findViewById(R.id.anchor_handle));
+ mTextSelection = new ActionBarTextSelection(this);
} else {
mTextSelection = new FloatingToolbarTextSelection(this, mLayerView);
}
mTextSelection.create();
// Determine whether we should restore tabs.
mLastSessionCrashed = updateCrashedState();
mShouldRestore = getSessionRestoreState(savedInstanceState);
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/TextSelectionHandle.java
+++ /dev/null
@@ -1,219 +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/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-import org.mozilla.gecko.gfx.LayerView;
-
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-
-/**
- * Text selection handles enable a user to change position of selected text in
- * Gecko's DOM structure.
- *
- * A text "Selection" or nsISelection object, has start and end positions,
- * referred to as Anchor and Focus objects.
- *
- * If the Anchor and Focus objects are at the same point, it represents a text
- * selection Caret, commonly diplayed as a blinking, vertical |.
- *
- * Anchor and Focus objects each represent a DOM node, and character offset
- * from the start of the node. The Anchor always refers to the start of the
- * Selection, and the Focus refers to its end.
- *
- * In LTR languages such as English, the Anchor is to the left of the Focus.
- * In RTL languages such as Hebrew, the Anchor is to the right of the Focus.
- *
- * For multi-line Selections, in both LTR and RTL languages, the Anchor starts
- * above the Focus.
- */
-class TextSelectionHandle extends ImageView implements View.OnTouchListener {
- private static final String LOGTAG = "GeckoTextSelectionHandle";
-
- public enum HandleType { ANCHOR, CARET, FOCUS };
-
- private final HandleType mHandleType;
- private final int mWidth;
- private final int mHeight;
- private final int mShadow;
-
- private float mLeft;
- private float mTop;
- private boolean mIsRTL;
- private PointF mGeckoPoint;
- private PointF mTouchStart;
-
- private RelativeLayout.LayoutParams mLayoutParams;
-
- private static final int IMAGE_LEVEL_LTR = 0;
- private static final int IMAGE_LEVEL_RTL = 1;
-
- public TextSelectionHandle(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOnTouchListener(this);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextSelectionHandle);
- int handleType = a.getInt(R.styleable.TextSelectionHandle_handleType, 0x01);
- a.recycle();
-
- if (handleType == 0x01)
- mHandleType = HandleType.ANCHOR;
- else if (handleType == 0x02)
- mHandleType = HandleType.CARET;
- else
- mHandleType = HandleType.FOCUS;
-
- mGeckoPoint = new PointF(0.0f, 0.0f);
- mTouchStart = new PointF(0.0f, 0.0f);
-
- mWidth = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_width);
- mHeight = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_height);
- mShadow = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_shadow);
- }
-
- private int getStatusBarHeight() {
- int result = 0;
- int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
- if (resourceId > 0) {
- result = getResources().getDimensionPixelSize(resourceId);
- }
- return result;
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- mTouchStart.x = event.getX();
- mTouchStart.y = event.getY();
- break;
- }
- case MotionEvent.ACTION_UP: {
- mTouchStart.x = 0;
- mTouchStart.y = 0;
-
- // Reposition handles to line up with ends of selection
- JSONObject args = new JSONObject();
- try {
- args.put("handleType", mHandleType.toString());
- } catch (Exception e) {
- Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Position");
- }
- GeckoAppShell.notifyObservers("TextSelection:Position", args.toString());
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- move(event.getRawX(), event.getRawY());
- break;
- }
- }
- return true;
- }
-
- private void move(float newX, float newY) {
- LayerView layerView = GeckoAppShell.getLayerView();
-
- // newX and newY are in screen coordinates, but mLeft/mTop are relative
- // to the ancestor (which is what LayerView is relative to also). So,
- // we need to adjust newX/newY. The |ancestorOrigin| variable computed
- // below is the origin of the ancestor relative to the screen coordinates,
- // so subtracting that from newY puts newY into the desired coordinate
- // space. We also need to include the offset amount of the touch location
- // relative to the top left of the handle (mTouchStart).
- int[] layerViewPosition = new int[2];
- layerView.getLocationOnScreen(layerViewPosition);
- float ancestorOrigin = layerViewPosition[1];
-
- mLeft = newX - mTouchStart.x;
- mTop = newY - mTouchStart.y - ancestorOrigin;
-
- // Send x coordinate on the right side of the start handle, left side of the end handle.
- float layerViewTranslation = layerView.getSurfaceTranslation();
- PointF geckoPoint = new PointF(mLeft + adjustLeftForHandle(),
- mTop - layerViewTranslation);
- geckoPoint = layerView.convertViewPointToLayerPoint(geckoPoint);
-
- JSONObject args = new JSONObject();
- try {
- args.put("handleType", mHandleType.toString());
- args.put("x", (int) geckoPoint.x);
- args.put("y", (int) geckoPoint.y);
- } catch (Exception e) {
- Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Move");
- }
- GeckoAppShell.notifyObservers("TextSelection:Move", args.toString());
-
- // If we're positioning a cursor, don't move the handle here. Gecko
- // will tell us the position of the caret, so we set the handle
- // position then. This allows us to lock the handle to wherever the
- // caret appears.
- if (mHandleType != HandleType.CARET) {
- setLayoutPosition();
- }
- }
-
- void positionFromGecko(int left, int top, boolean rtl) {
- LayerView layerView = GeckoAppShell.getLayerView();
- if (layerView == null) {
- Log.e(LOGTAG, "Can't position handle because layerView is null");
- return;
- }
-
- mGeckoPoint = new PointF(left, top);
- if (mIsRTL != rtl) {
- mIsRTL = rtl;
- setImageLevel(mIsRTL ? IMAGE_LEVEL_RTL : IMAGE_LEVEL_LTR);
- }
-
- ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
- repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
- }
-
- void repositionWithViewport(float x, float y, float zoom) {
- PointF viewPoint = new PointF((mGeckoPoint.x * zoom) - x,
- (mGeckoPoint.y * zoom) - y);
- mLeft = viewPoint.x - adjustLeftForHandle();
- mTop = viewPoint.y + GeckoAppShell.getLayerView().getSurfaceTranslation();
-
- setLayoutPosition();
- }
-
- private float adjustLeftForHandle() {
- if (mHandleType == HandleType.ANCHOR) {
- return mIsRTL ? mShadow : mWidth - mShadow;
- } else if (mHandleType == HandleType.CARET) {
- return mWidth / 2;
- } else {
- return mIsRTL ? mWidth - mShadow : mShadow;
- }
- }
-
- private void setLayoutPosition() {
- if (mLayoutParams == null) {
- mLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
- // Set negative right/bottom margins so that the handles can be dragged outside of
- // the content area (if they are dragged to the left/top, the dyanmic margins set
- // below will take care of that).
- mLayoutParams.rightMargin = 0 - mWidth;
- mLayoutParams.bottomMargin = 0 - mHeight;
- }
-
- mLayoutParams.leftMargin = (int) mLeft;
- mLayoutParams.topMargin = (int) mTop;
- setLayoutParams(mLayoutParams);
- }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -217,27 +217,42 @@ public class HomeConfigPrefsBackend impl
/**
* Iterate over all homepanels to verify that there is at least one default panel. If there is
* no default panel, set History as the default panel. (This is only relevant for two botched
* migrations where the history panel should have been made the default panel, but wasn't.)
*/
private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
int historyIndex = -1;
+ // If all panels are disabled, there is no default panel - this is the only valid state
+ // that has no default. We can use this flag to track whether any visible panels have been
+ // found.
+ boolean enabledPanelsFound = false;
+
for (int i = 0; i < jsonPanels.length(); i++) {
final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
if (panelConfig.isDefault()) {
return;
}
+ if (!panelConfig.isDisabled()) {
+ enabledPanelsFound = true;
+ }
+
if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
historyIndex = i;
}
}
+ if (!enabledPanelsFound) {
+ // No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration
+ // for more information).
+ return;
+ }
+
// Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
}
/**
* Removes a panel from the home panel config.
* If the removed panel was set as the default home panel, we provide a replacement for it.
@@ -250,46 +265,48 @@ public class HomeConfigPrefsBackend impl
* @param alwaysUnhide If true, the replacement panel will always be unhidden,
* otherwise only if we turn it into the new default panel.
* @return new array of updated JSON panels
* @throws JSONException
*/
private static JSONArray removePanel(Context context, JSONArray jsonPanels,
PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
boolean wasDefault = false;
+ boolean wasDisabled = false;
int replacementPanelIndex = -1;
boolean replacementWasDefault = false;
// JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
// the items we don't want deleted into a new array.
final JSONArray newJSONPanels = new JSONArray();
for (int i = 0; i < jsonPanels.length(); i++) {
final JSONObject panelJSON = jsonPanels.getJSONObject(i);
final PanelConfig panelConfig = new PanelConfig(panelJSON);
if (panelConfig.getType() == panelToRemove) {
// If this panel was the default we'll need to assign a new default:
wasDefault = panelConfig.isDefault();
+ wasDisabled = panelConfig.isDisabled();
} else {
if (panelConfig.getType() == replacementPanel) {
replacementPanelIndex = newJSONPanels.length();
if (panelConfig.isDefault()) {
replacementWasDefault = true;
}
}
newJSONPanels.put(panelJSON);
}
}
// Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
// to be the new default panel, since a hidden default panel doesn't make sense.
// This is to allow preserving the behaviour of the original reading list migration function.
- if (wasDefault || alwaysUnhide) {
+ if ((wasDefault || alwaysUnhide) && !wasDisabled) {
final JSONObject replacementPanelConfig;
if (wasDefault) {
// If the removed panel was the default, the replacement has to be made the new default
replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
} else {
final EnumSet<HomeConfig.PanelConfig.Flags> flags;
if (replacementWasDefault) {
// However if the replacement panel was already default, we need to preserve it's default status
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -685,17 +685,16 @@ gbjar.sources += ['java/org/mozilla/geck
'telemetry/TelemetryPing.java',
'telemetry/TelemetryPreferences.java',
'telemetry/TelemetryUploadService.java',
'TelemetryContract.java',
'text/FloatingActionModeCallback.java',
'text/FloatingToolbarTextSelection.java',
'text/TextAction.java',
'text/TextSelection.java',
- 'TextSelectionHandle.java',
'ThumbnailHelper.java',
'toolbar/AutocompleteHandler.java',
'toolbar/BackButton.java',
'toolbar/BrowserToolbar.java',
'toolbar/BrowserToolbarPhone.java',
'toolbar/BrowserToolbarPhoneBase.java',
'toolbar/BrowserToolbarTablet.java',
'toolbar/BrowserToolbarTabletBase.java',
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -37,18 +37,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mozilla.gecko.FormAssistPopup android:id="@+id/form_assist_popup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
- <include layout="@layout/text_selection_handles"/>
-
<FrameLayout android:id="@+id/camera_layout"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true">
</FrameLayout>
<view class="org.mozilla.gecko.media.VideoPlayer" android:id="@+id/video_player"
deleted file mode 100644
--- a/mobile/android/base/resources/layout/text_selection_handles.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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/. -->
-
-<merge xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:gecko="http://schemas.android.com/apk/res-auto">
-
- <org.mozilla.gecko.TextSelectionHandle android:id="@+id/anchor_handle"
- android:layout_width="@dimen/text_selection_handle_width"
- android:layout_height="@dimen/text_selection_handle_height"
- android:src="@drawable/handle_anchor_level"
- android:visibility="gone"
- gecko:handleType="start"/>
-
- <org.mozilla.gecko.TextSelectionHandle android:id="@+id/caret_handle"
- android:layout_width="@dimen/text_selection_handle_width"
- android:layout_height="@dimen/text_selection_handle_height"
- android:src="@drawable/handle_middle"
- android:visibility="gone"
- gecko:handleType="middle"/>
-
- <org.mozilla.gecko.TextSelectionHandle android:id="@+id/focus_handle"
- android:layout_width="@dimen/text_selection_handle_width"
- android:layout_height="@dimen/text_selection_handle_height"
- android:src="@drawable/handle_focus_level"
- android:visibility="gone"
- gecko:handleType="end"/>
-</merge>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -88,24 +88,16 @@
<flag name="tabs_private" value ="0x01" />
</attr>
</declare-styleable>
<declare-styleable name="TabCounter">
<attr name="android:layout"/>
</declare-styleable>
- <declare-styleable name="TextSelectionHandle">
- <attr name="handleType">
- <flag name="start" value="0x01" />
- <flag name="middle" value="0x02" />
- <flag name="end" value="0x03" />
- </attr>
- </declare-styleable>
-
<declare-styleable name="PrivateBrowsing">
<attr name="state_private" format="boolean"/>
</declare-styleable>
<declare-styleable name="LightweightTheme">
<attr name="state_light" format="boolean"/>
<attr name="state_dark" format="boolean"/>
<attr name="autoUpdateTheme" format="boolean"/>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -134,19 +134,16 @@
<dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
<dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
<dimen name="tabs_panel_indicator_width">60dp</dimen>
<dimen name="tabs_panel_button_width">48dp</dimen>
<dimen name="tabs_strip_height">48dp</dimen>
<dimen name="tabs_strip_button_width">100dp</dimen>
<dimen name="tabs_strip_button_padding">18dp</dimen>
<dimen name="tabs_strip_shadow_size">1dp</dimen>
- <dimen name="text_selection_handle_width">47dp</dimen>
- <dimen name="text_selection_handle_height">58dp</dimen>
- <dimen name="text_selection_handle_shadow">11dp</dimen>
<dimen name="validation_message_height">50dp</dimen>
<dimen name="validation_message_margin_top">6dp</dimen>
<dimen name="tab_thumbnail_width">121dp</dimen>
<dimen name="tab_thumbnail_height">90dp</dimen>
<dimen name="tab_panel_column_width">129dp</dimen>
<dimen name="tab_panel_grid_padding">20dp</dimen>
<dimen name="tab_panel_grid_vspacing">20dp</dimen>
deleted file mode 100644
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ /dev/null
@@ -1,1482 +0,0 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
-XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
-
-// Define elements that bound phone number containers.
-const PHONE_NUMBER_CONTAINERS = "td,div";
-const DEFER_CLOSE_TRIGGER_MS = 125; // Grace period delay before deferred _closeSelection()
-
-// Gecko AccessibleCaret pref names.
-const PREF_GECKO_ACCESSIBLECARET_ENABLED = "layout.accessiblecaret.enabled";
-
-var SelectionHandler = {
-
- // Successful startSelection() or attachCaret().
- ERROR_NONE: "",
-
- // Error codes returned during startSelection().
- START_ERROR_INVALID_MODE: "Invalid selection mode requested.",
- START_ERROR_NONTEXT_INPUT: "Target element by definition contains no text.",
- START_ERROR_NO_WORD_SELECTED: "No word selected at point.",
- START_ERROR_SELECT_WORD_FAILED: "Word selection at point failed.",
- START_ERROR_SELECT_ALL_PARAGRAPH_FAILED: "Select-All Paragraph failed.",
- START_ERROR_NO_SELECTION: "Selection performed, but nothing resulted.",
- START_ERROR_PROXIMITY: "Selection target and result seem unrelated.",
- START_ERROR_SELECTIONCARETS_ENABLED: "Native selectionCarets requested while Gecko enabled.",
-
- // Error codes returned during attachCaret().
- ATTACH_ERROR_INCOMPATIBLE: "Element disabled, handled natively, or not editable.",
- ATTACH_ERROR_TOUCHCARET_ENABLED: "Native touchCaret requested while Gecko enabled.",
-
- HANDLE_TYPE_ANCHOR: "ANCHOR",
- HANDLE_TYPE_CARET: "CARET",
- HANDLE_TYPE_FOCUS: "FOCUS",
-
- TYPE_NONE: 0,
- TYPE_CURSOR: 1,
- TYPE_SELECTION: 2,
-
- SELECT_ALL: 0,
- SELECT_AT_POINT: 1,
-
- // Gecko TouchCaret/SelectionCaret pref values.
- _accessibleCaretEnabledValue: null,
- _selectionCaretEnabledValue: null,
-
- // Keeps track of data about the dimensions of the selection. Coordinates
- // stored here are relative to the _contentWindow window.
- _cache: { anchorPt: {}, focusPt: {} },
- _targetIsRTL: false,
- _anchorIsRTL: false,
- _focusIsRTL: false,
-
- _activeType: 0, // TYPE_NONE
- _selectionPrivate: null, // private selection reference
- _selectionID: null, // Unique Selection ID
-
- _draggingHandles: false, // True while user drags text selection handles
- _dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
- _dragStartFocusOffset: null, // Editables need initial pos during HandleMove events
-
- _ignoreCompositionChanges: false, // Persist caret during IME composition updates
- _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
- _deferCloseTimer: null, // Used to defer _closeSelection() actions during programmatic changes
-
- // TargetElement changes (text <--> no text) trigger actionbar UI update
- _prevTargetElementHasText: null,
-
- // The window that holds the selection (can be a sub-frame)
- get _contentWindow() {
- if (this._contentWindowRef)
- return this._contentWindowRef.get();
- return null;
- },
-
- set _contentWindow(aContentWindow) {
- this._contentWindowRef = Cu.getWeakReference(aContentWindow);
- },
-
- // Main target element, always provides editor for editables.
- get _targetElement() {
- if (this._targetElementRef)
- return this._targetElementRef.get();
- return null;
- },
-
- set _targetElement(aTargetElement) {
- this._targetElementRef = Cu.getWeakReference(aTargetElement);
- },
-
- // Alternate target element. Always provides public DOM node for editables
- // that contain anonymous inner content structures.
- _targetDOMCaretNode: null,
-
- get _domWinUtils() {
- return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
- getInterface(Ci.nsIDOMWindowUtils);
- },
-
- // Provides UUID service for selection ID's.
- get _idService() {
- delete this._idService;
- return this._idService = Cc["@mozilla.org/uuid-generator;1"].
- getService(Ci.nsIUUIDGenerator);
- },
-
- // Are we supporting Accessible-core or native-Java carets?
- get _accessibleCaretEnabled() {
- if (this._accessibleCaretEnabledValue == null) {
- try {
- this._accessibleCaretEnabledValue = Services.prefs.getBoolPref(PREF_GECKO_ACCESSIBLECARET_ENABLED);
- } catch (unused) { }
- Services.prefs.addObserver(PREF_GECKO_ACCESSIBLECARET_ENABLED, function() {
- SelectionHandler._accessibleCaretEnabledValue =
- Services.prefs.getBoolPref(PREF_GECKO_ACCESSIBLECARET_ENABLED);
- }, false);
- }
- return this._accessibleCaretEnabledValue;
- },
-
- _addObservers: function sh_addObservers() {
- Services.obs.addObserver(this, "Gesture:SingleTap", false);
- Services.obs.addObserver(this, "Tab:Selected", false);
- Services.obs.addObserver(this, "TextSelection:Move", false);
- Services.obs.addObserver(this, "TextSelection:Position", false);
- Services.obs.addObserver(this, "TextSelection:End", false);
- Services.obs.addObserver(this, "TextSelection:Action", false);
- Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
-
- BrowserApp.deck.addEventListener("pagehide", this, false);
- BrowserApp.deck.addEventListener("blur", this, true);
- BrowserApp.deck.addEventListener("scroll", this, true);
- },
-
- _removeObservers: function sh_removeObservers() {
- Services.obs.removeObserver(this, "Gesture:SingleTap");
- Services.obs.removeObserver(this, "Tab:Selected");
- Services.obs.removeObserver(this, "TextSelection:Move");
- Services.obs.removeObserver(this, "TextSelection:Position");
- Services.obs.removeObserver(this, "TextSelection:End");
- Services.obs.removeObserver(this, "TextSelection:Action");
- Services.obs.removeObserver(this, "TextSelection:LayerReflow");
-
- BrowserApp.deck.removeEventListener("pagehide", this, false);
- BrowserApp.deck.removeEventListener("blur", this, true);
- BrowserApp.deck.removeEventListener("scroll", this, true);
- },
-
- observe: function sh_observe(aSubject, aTopic, aData) {
- // Ignore all but selectionListener notifications during deferred _closeSelection().
- if (this._deferCloseTimer) {
- return;
- }
-
- switch (aTopic) {
- // Update selectionListener and handle/caret positions, on page reflow
- // (keyboard open/close, dynamic DOM changes, orientation updates, etc).
- case "TextSelection:LayerReflow": {
- if (this._activeType == this.TYPE_SELECTION) {
- this._updateSelectionListener();
- }
- if (this._activeType != this.TYPE_NONE) {
- this._positionHandlesOnChange();
- }
- break;
- }
-
- case "Gesture:SingleTap": {
- if (this._activeType == this.TYPE_CURSOR) {
- // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
- // We're guaranteed to call this first, because this observer was added last
- this._deactivate();
- }
- break;
- }
-
- case "Tab:Selected":
- this._closeSelection();
- break;
-
- case "TextSelection:End":
- let data = JSON.parse(aData);
- // End the requested selection only.
- if (this._selectionID === data.selectionID) {
- this._closeSelection();
- }
- break;
-
- case "TextSelection:Action":
- for (let type in this.actions) {
- if (this.actions[type].id == aData) {
- this.actions[type].action(this._targetElement);
- break;
- }
- }
- break;
-
- case "TextSelection:Move": {
- let data = JSON.parse(aData);
- if (this._activeType == this.TYPE_SELECTION) {
- this._startDraggingHandles();
- this._moveSelection(data.handleType, new Point(data.x, data.y));
-
- } else if (this._activeType == this.TYPE_CURSOR) {
- this._startDraggingHandles();
-
- // Ignore IMM composition notifications when caret movement starts
- this._ignoreCompositionChanges = true;
- this._moveCaret(data.x, data.y);
-
- // Move the handle directly under the caret
- this._positionHandles();
- }
- break;
- }
-
- case "TextSelection:Position": {
- if (this._activeType == this.TYPE_SELECTION) {
- this._startDraggingHandles();
- this._ensureSelectionDirection();
- this._stopDraggingHandles();
- this._positionHandles();
-
- // Changes to handle position can affect selection context and actionbar display
- this._updateMenu();
-
- } else if (this._activeType == this.TYPE_CURSOR) {
- // Act on IMM composition notifications after caret movement ends
- this._ignoreCompositionChanges = false;
- this._stopDraggingHandles();
- this._positionHandles();
-
- } else {
- Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
- }
-
- break;
- }
-
- case "TextSelection:Get":
- Messaging.sendRequest({
- type: "TextSelection:Data",
- requestId: aData,
- text: this._getSelectedText()
- });
- break;
- }
- },
-
- // Ignore selectionChange notifications during handle dragging, disable dynamic
- // IME text compositions (autoSuggest, autoCorrect, etc)
- _startDraggingHandles: function sh_startDraggingHandles() {
- if (!this._draggingHandles) {
- this._draggingHandles = true;
- let selection = this._getSelection();
- this._dragStartAnchorOffset = selection.anchorOffset;
- this._dragStartFocusOffset = selection.focusOffset;
- Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: true });
- }
- },
-
- // Act on selectionChange notifications when not dragging handles, allow dynamic
- // IME text compositions (autoSuggest, autoCorrect, etc)
- _stopDraggingHandles: function sh_stopDraggingHandles() {
- if (this._draggingHandles) {
- this._draggingHandles = false;
- this._dragStartAnchorOffset = null;
- this._dragStartFocusOffset = null;
- Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: false });
- }
- },
-
- handleEvent: function sh_handleEvent(aEvent) {
- // Ignore all but selectionListener notifications during deferred _closeSelection().
- if (this._deferCloseTimer) {
- return;
- }
-
- switch (aEvent.type) {
- case "scroll":
- // Maintain position when top-level document is scrolled
- this._positionHandlesOnChange();
- break;
-
- case "pagehide": {
- // We only care about events on the selected tab.
- let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView);
- if (tab == BrowserApp.selectedTab) {
- this._closeSelection();
- }
- break;
- }
-
- case "blur":
- this._closeSelection();
- break;
-
- // Update caret position on keyboard activity
- case "keyup":
- // Not generated by Swiftkeyboard
- case "compositionupdate":
- case "compositionend":
- // Generated by SwiftKeyboard, et. al.
- if (!this._ignoreCompositionChanges) {
- this._positionHandles();
- }
- break;
- }
- },
-
- /** Returns true if the provided element can be selected in text selection, false otherwise. */
- canSelect: function sh_canSelect(aElement) {
- return !(aElement instanceof Ci.nsIDOMHTMLButtonElement ||
- aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
- aElement instanceof Ci.nsIDOMHTMLImageElement ||
- aElement instanceof Ci.nsIDOMHTMLMediaElement) &&
- aElement.style.MozUserSelect != 'none';
- },
-
- _getScrollPos: function sh_getScrollPos() {
- // Get the current display position
- let scrollX = {}, scrollY = {};
- this._contentWindow.top.QueryInterface(Ci.nsIInterfaceRequestor).
- getInterface(Ci.nsIDOMWindowUtils).getScrollXY(false, scrollX, scrollY);
- return {
- X: scrollX.value,
- Y: scrollY.value
- };
- },
-
- /**
- * Add a selection listener to monitor for external selection changes.
- */
- _addSelectionListener: function(selection) {
- this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate);
- this._selectionPrivate.addSelectionListener(this);
- },
-
- /**
- * The nsISelection object for an editable can change during DOM mutations,
- * causing us to stop receiving selectionChange notifications.
- *
- * We can detect that after a layer-reflow event, and dynamically update the
- * listener.
- */
- _updateSelectionListener: function() {
- if (!(this._targetElement instanceof Ci.nsIDOMNSEditableElement)) {
- return;
- }
-
- let selection = this._getSelection();
- if (this._selectionPrivate != selection.QueryInterface(Ci.nsISelectionPrivate)) {
- this._removeSelectionListener();
- this._addSelectionListener(selection);
- }
- },
-
- /**
- * Remove the selection listener.
- */
- _removeSelectionListener: function() {
- this._selectionPrivate.removeSelectionListener(this);
- this._selectionPrivate = null;
- },
-
- /**
- * Observe and react to programmatic SelectionChange notifications.
- */
- notifySelectionChanged: function sh_notifySelectionChanged(aDocument, aSelection, aReason) {
- // Cancel any in-progress / deferred _closeSelection() action.
- this._cancelDeferredCloseSelection();
-
- // Ignore selectionChange notifications during handle movements
- if (this._draggingHandles) {
- return;
- }
-
- // If the selection was collapsed to Start or to End, always close it
- if ((aReason & Ci.nsISelectionListener.COLLAPSETOSTART_REASON) ||
- (aReason & Ci.nsISelectionListener.COLLAPSETOEND_REASON)) {
- this._closeSelection();
- return;
- }
-
- // If selected text no longer exists, schedule a deferred close action.
- if (!aSelection.toString()) {
- this._deferCloseSelection();
- return;
- }
-
- // Update the selection handle positions.
- this._positionHandles();
- },
-
- /*
- * Called from browser.js when the user long taps on text or chooses
- * the "Select Word" context menu item. Initializes SelectionHandler,
- * starts a selection, and positions the text selection handles.
- *
- * @param aOptions list of options describing how to start selection
- * Options include:
- * mode - SELECT_ALL to select everything in the target
- * element, or SELECT_AT_POINT to select a word.
- * x - The x-coordinate for SELECT_AT_POINT.
- * y - The y-coordinate for SELECT_AT_POINT.
- */
- startSelection: function sh_startSelection(aElement, aOptions = { mode: SelectionHandler.SELECT_ALL }) {
- // Disable Native touchCarets if Gecko AccessibleCaret enabled.
- if (this._accessibleCaretEnabled) {
- return this.START_ERROR_SELECTIONCARETS_ENABLED;
- }
-
- // Clear out any existing active selection
- this._closeSelection();
-
- if (this._isNonTextInputElement(aElement)) {
- return this.START_ERROR_NONTEXT_INPUT;
- }
-
- const focus = Services.focus.focusedWindow;
- if (focus) {
- // Make sure any previous focus is cleared.
- Services.focus.clearFocus(focus);
- }
-
- this._initTargetInfo(aElement, this.TYPE_SELECTION);
-
- // Perform the appropriate selection method, if we can't determine method, or it fails, return
- let selectionResult = this._performSelection(aOptions);
- if (selectionResult !== this.ERROR_NONE) {
- this._deactivate();
- return selectionResult;
- }
-
- // Double check results of successful selection operation
- let selection = this._getSelection();
- if (!selection ||
- selection.rangeCount == 0 ||
- selection.getRangeAt(0).collapsed ||
- this._getSelectedText().length == 0) {
- this._deactivate();
- return this.START_ERROR_NO_SELECTION;
- }
-
- // Add a listener to end the selection if it's removed programatically
- this._addSelectionListener(selection);
- this._activeType = this.TYPE_SELECTION;
-
- // Figure out the distance between the selection and the click
- let scroll = this._getScrollPos();
- let positions = this._getHandlePositions(scroll);
-
- if (aOptions.mode == this.SELECT_AT_POINT &&
- !this._selectionNearClick(scroll.X + aOptions.x, scroll.Y + aOptions.y, positions)) {
- this._closeSelection();
- return this.START_ERROR_PROXIMITY;
- }
-
- // Determine position and show handles, open actionbar
- this._positionHandles(positions);
- Messaging.sendRequest({
- selectionID: this._selectionID,
- type: "TextSelection:ShowHandles",
- handles: [this.HANDLE_TYPE_ANCHOR, this.HANDLE_TYPE_FOCUS]
- });
- this._updateMenu();
- return this.ERROR_NONE;
- },
-
- /*
- * Called to perform a selection operation, given a target element, selection method, starting point etc.
- */
- _performSelection: function sh_performSelection(aOptions) {
- if (aOptions.mode == this.SELECT_AT_POINT) {
- // Clear any ranges selected outside SelectionHandler, by code such as Find-In-Page.
- this._contentWindow.getSelection().removeAllRanges();
- try {
- if (!this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
- return this.START_ERROR_NO_WORD_SELECTED;
- }
- } catch (e) {
- return this.START_ERROR_SELECT_WORD_FAILED;
- }
-
- // Perform additional phone-number "smart selection".
- if (this._isPhoneNumber(this._getSelection().toString())) {
- this._selectSmartPhoneNumber();
- }
-
- return this.ERROR_NONE;
- }
-
- // Only selectAll() assumed from this point.
- if (aOptions.mode != this.SELECT_ALL) {
- return this.START_ERROR_INVALID_MODE;
- }
-
- // HTMLPreElement is a #text node, SELECT_ALL implies entire paragraph
- if (this._targetElement instanceof HTMLPreElement) {
- try {
- this._domWinUtils.selectAtPoint(1, 1, Ci.nsIDOMWindowUtils.SELECT_PARAGRAPH);
- return this.ERROR_NONE;
- } catch (e) {
- return this.START_ERROR_SELECT_ALL_PARAGRAPH_FAILED;
- }
- }
-
- // Else default to selectALL Document
- let editor = this._getEditor();
- if (editor) {
- editor.selectAll();
- } else {
- this._getSelectionController().selectAll();
- }
-
- // Selection is entire HTMLHtmlElement, remove any trailing document whitespace
- let selection = this._getSelection();
- let lastNode = selection.focusNode;
- while (lastNode && lastNode.lastChild) {
- lastNode = lastNode.lastChild;
- }
-
- if (lastNode instanceof Text) {
- try {
- selection.extend(lastNode, lastNode.length);
- } catch (e) {
- Cu.reportError("SelectionHandler.js: _performSelection() whitespace trim fails: lastNode[" + lastNode +
- "] lastNode.length[" + lastNode.length + "]");
- }
- }
-
- return this.ERROR_NONE;
- },
-
- /*
- * Called to expand a selection that appears to represent a phone number. This enhances the basic
- * SELECT_WORDNOSPACE logic employed in performSelection() in response to long-tap / selecting text.
- */
- _selectSmartPhoneNumber: function() {
- this._extendPhoneNumberSelection("forward");
- this._reversePhoneNumberSelectionDir();
-
- this._extendPhoneNumberSelection("backward");
- this._reversePhoneNumberSelectionDir();
- },
-
- /*
- * Extend the current phone number selection in the requested direction.
- */
- _extendPhoneNumberSelection: function(direction) {
- let selection = this._getSelection();
-
- // Extend the phone number selection until we find a boundry.
- while (true) {
- // Save current focus position, and extend the selection.
- let focusNode = selection.focusNode;
- let focusOffset = selection.focusOffset;
- selection.modify("extend", direction, "character");
-
- // If the selection doesn't change, (can't extend further), we're done.
- if (selection.focusNode == focusNode && selection.focusOffset == focusOffset) {
- return;
- }
-
- // Don't extend past a valid phone number.
- if (!this._isPhoneNumber(selection.toString().trim())) {
- // Backout the undesired selection extend, and we're done.
- selection.collapse(selection.anchorNode, selection.anchorOffset);
- selection.extend(focusNode, focusOffset);
- return;
- }
-
- // Don't extend the selection into a new container.
- if (selection.focusNode != focusNode) {
- let nextContainer = (selection.focusNode instanceof Text) ?
- selection.focusNode.parentNode : selection.focusNode;
- if (nextContainer.matches &&
- nextContainer.matches(PHONE_NUMBER_CONTAINERS)) {
- // Backout the undesired selection extend, and we're done.
- selection.collapse(selection.anchorNode, selection.anchorOffset);
- selection.extend(focusNode, focusOffset);
- return
- }
- }
- }
- },
-
- /*
- * Reverse the the selection direction, swapping anchorNode <-+-> focusNode.
- */
- _reversePhoneNumberSelectionDir: function(direction) {
- let selection = this._getSelection();
-
- let anchorNode = selection.anchorNode;
- let anchorOffset = selection.anchorOffset;
- selection.collapse(selection.focusNode, selection.focusOffset);
- selection.extend(anchorNode, anchorOffset);
- },
-
- /* Return true if the current selection (given by aPositions) is near to where the coordinates passed in */
- _selectionNearClick: function(aX, aY, aPositions) {
- let distance = 0;
-
- // Check if the click was in the bounding box of the selection handles
- if (aPositions[0].left < aX && aX < aPositions[1].left
- && aPositions[0].top < aY && aY < aPositions[1].top) {
- distance = 0;
- } else {
- // If it was outside, check the distance to the center of the selection
- let selectposX = (aPositions[0].left + aPositions[1].left) / 2;
- let selectposY = (aPositions[0].top + aPositions[1].top) / 2;
-
- let dx = Math.abs(selectposX - aX);
- let dy = Math.abs(selectposY - aY);
- distance = dx + dy;
- }
-
- let maxSelectionDistance = Services.prefs.getIntPref("browser.ui.selection.distance");
- return (distance < maxSelectionDistance);
- },
-
- /* Reads a value from an action. If the action defines the value as a function, will return the result of calling
- the function. Otherwise, will return the value itself. If the value isn't defined for this action, will return a default */
- _getValue: function(obj, name, defaultValue) {
- if (!(name in obj))
- return defaultValue;
-
- if (typeof obj[name] == "function")
- return obj[name](this._targetElement);
-
- return obj[name];
- },
-
- addAction: function(action) {
- if (!action.id)
- action.id = uuidgen.generateUUID().toString()
-
- if (this.actions[action.id])
- throw "Action with id " + action.id + " already added";
-
- // Update actions list and actionbar UI if active.
- this.actions[action.id] = action;
- this._updateMenu();
- return action.id;
- },
-
- removeAction: function(id) {
- // Update actions list and actionbar UI if active.
- delete this.actions[id];
- this._updateMenu();
- },
-
- _updateMenu: function() {
- if (this._activeType == this.TYPE_NONE) {
- return;
- }
-
- // Update actionbar UI.
- let actions = [];
- for (let type in this.actions) {
- let action = this.actions[type];
- if (action.selector.matches(this._targetElement)) {
- let a = {
- id: action.id,
- label: this._getValue(action, "label", ""),
- icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
- showAsAction: this._getValue(action, "showAsAction", true),
- order: this._getValue(action, "order", 0)
- };
- actions.push(a);
- }
- }
-
- actions.sort((a, b) => b.order - a.order);
-
- Messaging.sendRequest({
- type: "TextSelection:Update",
- actions: actions
- });
- },
-
- /*
- * Actionbar methods.
- */
- actions: {
- SELECT_ALL: {
- label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
- id: "selectall_action",
- icon: "drawable://ab_select_all",
- action: function(aElement) {
- // Use the public DOMNode for startSelection(), not any anonymous inner.
- SelectionHandler.startSelection(SelectionHandler._targetDOMCaretNode);
- UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
- },
- order: 5,
- selector: {
- matches: function(aElement) {
- return (aElement.textLength != 0);
- }
- }
- },
-
- CUT: {
- label: Strings.browser.GetStringFromName("contextmenu.cut"),
- id: "cut_action",
- icon: "drawable://ab_cut",
- action: function(aElement) {
- let start = aElement.selectionStart;
- let end = aElement.selectionEnd;
-
- SelectionHandler.copySelection();
- aElement.value = aElement.value.substring(0, start) + aElement.value.substring(end)
-
- // copySelection closes the selection. Show a caret where we just cut the text.
- SelectionHandler.attachCaret(aElement);
- UITelemetry.addEvent("action.1", "actionbar", null, "cut");
- },
- order: 4,
- selector: {
- matches: function(aElement) {
- // Disallow cut for contentEditable elements (until Bug 1112276 is fixed).
- return !aElement.isContentEditable && SelectionHandler.isElementEditableText(aElement) ?
- SelectionHandler.isSelectionActive() : false;
- }
- }
- },
-
- COPY: {
- label: Strings.browser.GetStringFromName("contextmenu.copy"),
- id: "copy_action",
- icon: "drawable://ab_copy",
- action: function() {
- SelectionHandler.copySelection();
- UITelemetry.addEvent("action.1", "actionbar", null, "copy");
- },
- order: 3,
- selector: {
- matches: function(aElement) {
- // Don't include "copy" for password fields.
- if (aElement instanceof Ci.nsIDOMHTMLInputElement && (aElement.type === "password")) {
- return false;
- }
- return SelectionHandler.isSelectionActive();
- }
- }
- },
-
- PASTE: {
- label: Strings.browser.GetStringFromName("contextmenu.paste"),
- id: "paste_action",
- icon: "drawable://ab_paste",
- action: function(aElement) {
- if (aElement) {
- let target = SelectionHandler._getEditor();
- aElement.focus();
- target.paste(Ci.nsIClipboard.kGlobalClipboard);
- SelectionHandler._closeSelection();
- UITelemetry.addEvent("action.1", "actionbar", null, "paste");
- }
- },
- order: 2,
- selector: {
- matches: function(aElement) {
- if (SelectionHandler.isElementEditableText(aElement)) {
- let flavors = ["text/unicode"];
- return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
- }
- return false;
- }
- }
- },
-
- SHARE: {
- label: Strings.browser.GetStringFromName("contextmenu.share"),
- id: "share_action",
- icon: "drawable://ic_menu_share",
- action: function() {
- SelectionHandler.shareSelection();
- UITelemetry.addEvent("action.1", "actionbar", null, "share");
- },
- selector: {
- matches: function() {
- if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
- return false;
- }
-
- return SelectionHandler.isSelectionActive();
- }
- }
- },
-
- SEARCH_ADD: {
- id: "search_add_action",
- label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
- icon: "drawable://ab_add_search_engine",
-
- selector: {
- matches: function(element) {
- if(!(element instanceof HTMLInputElement)) {
- return false;
- }
- let form = element.form;
- if (!form || element.type == "password") {
- return false;
- }
-
- // These are the following types of forms we can create keywords for:
- //
- // method encoding type can create keyword
- // GET * YES
- // * YES
- // POST * YES
- // POST application/x-www-form-urlencoded YES
- // POST text/plain NO ( a little tricky to do)
- // POST multipart/form-data NO
- // POST everything else YES
- let method = form.method.toUpperCase();
- return (method == "GET" || method == "") ||
- (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
- },
- },
-
- action: function(element) {
- UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
- SearchEngines.addEngine(element);
- },
- },
-
- SEARCH: {
- label: function() {
- return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
- },
- id: "search_action",
- icon: "drawable://ab_search",
- action: function() {
- SelectionHandler.searchSelection();
- SelectionHandler._closeSelection();
- UITelemetry.addEvent("action.1", "actionbar", null, "search");
- },
- order: 1,
- selector: {
- matches: function() {
- return SelectionHandler.isSelectionActive();
- }
- }
- },
-
- CALL: {
- label: Strings.browser.GetStringFromName("contextmenu.call"),
- id: "call_action",
- icon: "drawable://phone",
- action: function() {
- SelectionHandler.callSelection();
- UITelemetry.addEvent("action.1", "actionbar", null, "call");
- },
- order: 1,
- selector: {
- matches: function () {
- return SelectionHandler._getSelectedPhoneNumber() != null;
- }
- }
- }
- },
-
- /*
- * Called by BrowserEventHandler when the user taps in a form input.
- * Initializes SelectionHandler and positions the caret handle.
- *
- * @param aX, aY tap location in client coordinates.
- */
- attachCaret: function sh_attachCaret(aElement) {
- // Disable Native touchCarets if Gecko AccessibleCaret enabled.
- if (this._accessibleCaretEnabled) {
- return this.ATTACH_ERROR_TOUCHCARET_ENABLED;
- }
-
- // Clear out any existing active selection
- this._closeSelection();
-
- // Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
- if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
- return this.ATTACH_ERROR_INCOMPATIBLE;
- }
-
- this._initTargetInfo(aElement, this.TYPE_CURSOR);
-
- // Caret-specific observer/listeners
- BrowserApp.deck.addEventListener("keyup", this, false);
- BrowserApp.deck.addEventListener("compositionupdate", this, false);
- BrowserApp.deck.addEventListener("compositionend", this, false);
-
- this._activeType = this.TYPE_CURSOR;
-
- // Determine position and show caret, open actionbar
- this._positionHandles();
- Messaging.sendRequest({
- selectionID: this._selectionID,
- type: "TextSelection:ShowHandles",
- handles: [this.HANDLE_TYPE_CARET]
- });
- this._updateMenu();
-
- return this.ERROR_NONE;
- },
-
- /**
- * <input> editables of type=number are special cases, bearing unique anonymous
- * internal content to facilitate up/down arrow UI controls. We will maintain a
- * reference to their public DOMNode, as well as their internal node, which
- * holds reference to it's editor.
- */
- _setTargetElements: function(element) {
- // Default, both values are the same.
- this._targetDOMCaretNode = element;
- this._targetElement = element;
- if (element.type !== "number") {
- return;
- }
-
- // Set the editor bearing anonymous inner <input> element.
- let editorNode = Services.focus.focusedElement;
- if (editorNode instanceof HTMLInputElement && editorNode.editor) {
- this._targetElement = editorNode;
- }
- return;
- },
-
- // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
- _initTargetInfo: function sh_initTargetInfo(aElement, aSelectionType) {
- if (aElement instanceof Ci.nsIDOMNSEditableElement) {
- if (aSelectionType === this.TYPE_SELECTION) {
- // Blur the targetElement to force IME code to undo previous style compositions
- // (visible underlines / etc generated by autoCorrection, autoSuggestion)
- aElement.blur();
- }
- // Ensure targetElement is now focused normally
- aElement.focus();
- }
- this._setTargetElements(aElement);
-
- this._selectionID = this._idService.generateUUID().toString();
- this._stopDraggingHandles();
- this._contentWindow = aElement.ownerDocument.defaultView;
- this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
-
- this._addObservers();
- },
-
- _getSelection: function sh_getSelection() {
- if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
- return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selection;
- else
- return this._contentWindow.getSelection();
- },
-
- _getSelectedText: function sh_getSelectedText() {
- if (!this._contentWindow)
- return "";
-
- let selection = this._getSelection();
- if (!selection)
- return "";
-
- if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
- return selection.QueryInterface(Ci.nsISelectionPrivate).
- toStringWithFormat("text/plain", Ci.nsIDocumentEncoder.OutputPreformatted | Ci.nsIDocumentEncoder.OutputRaw, 0);
- }
-
- return selection.toString().trim();
- },
-
- _getEditor: function sh_getEditor() {
- if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
- return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
- }
- return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIEditingSession)
- .getEditorForWindow(this._contentWindow);
- },
-
- _getSelectionController: function sh_getSelectionController() {
- if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
- return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selectionController;
- else
- return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
- getInterface(Ci.nsIWebNavigation).
- QueryInterface(Ci.nsIInterfaceRequestor).
- getInterface(Ci.nsISelectionDisplay).
- QueryInterface(Ci.nsISelectionController);
- },
-
- // Used by the contextmenu "matches" functions in ClipboardHelper
- isSelectionActive: function sh_isSelectionActive() {
- return (this._activeType == this.TYPE_SELECTION);
- },
-
- isElementEditableText: function (aElement) {
- return (((aElement instanceof HTMLInputElement &&
- (aElement.mozIsTextField(false) || aElement.type === "number")) ||
- (aElement instanceof HTMLTextAreaElement)) && !aElement.readOnly) ||
- aElement.isContentEditable;
- },
-
- _isNonTextInputElement: function(aElement) {
- return (aElement instanceof HTMLInputElement &&
- !(aElement.mozIsTextField(false) || aElement.type === "number"));
- },
-
- /*
- * Moves the selection as the user drags a handle.
- * @param handleType: Specifies either the anchor or the focus handle.
- * @param handlePt: selection point in client coordinates.
- */
- _moveSelection: function sh_moveSelection(handleType, handlePt) {
- let isAnchorHandle = (handleType == this.HANDLE_TYPE_ANCHOR);
-
- // Determine new caret position from handlePt, exit if user
- // moved it offscreen.
- let viewOffset = this._getViewOffset();
- let ptX = handlePt.x - viewOffset.x;
- let ptY = handlePt.y - viewOffset.y;
- let cwd = this._contentWindow.document;
- let caretPos = cwd.caretPositionFromPoint(ptX, ptY);
- if (!caretPos) {
- return;
- }
-
- // Constrain text selection within editable elements.
- let targetIsEditable = this._targetElement instanceof Ci.nsIDOMNSEditableElement;
- if (targetIsEditable && (caretPos.offsetNode != this._targetDOMCaretNode)) {
- return;
- }
-
- // Update the Selection for editable elements. Selection Change
- // logic is the same, regardless of RTL/LTR. Selection direction is
- // maintained always forward (startOffset <= endOffset).
- if (targetIsEditable) {
- let start = this._dragStartAnchorOffset;
- let end = this._dragStartFocusOffset;
- if (isAnchorHandle) {
- start = caretPos.offset;
- } else {
- end = caretPos.offset;
- }
- if (start > end) {
- [start, end] = [end, start];
- }
- this._targetElement.setSelectionRange(start, end);
- return;
- }
-
- // Update the Selection for non-editable elements. Selection Change
- // logic is the same, regardless of RTL/LTR. Selection direction internally
- // can finish reversed by user drag. ie: Forward is (a,o ---> f,o),
- // and reversed is (a,o <--- f,o).
- let selection = this._getSelection();
- if (isAnchorHandle) {
- let focusNode = selection.focusNode;
- let focusOffset = selection.focusOffset;
- selection.collapse(caretPos.offsetNode, caretPos.offset);
- selection.extend(focusNode, focusOffset);
- } else {
- selection.extend(caretPos.offsetNode, caretPos.offset);
- }
- },
-
- _moveCaret: function sh_moveCaret(aX, aY) {
- // Get rect of text inside element
- let range = document.createRange();
- range.selectNodeContents(this._getEditor().rootElement);
- let textBounds = range.getBoundingClientRect();
-
- // Get rect of editor
- let editorBounds = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_EDITOR_RECT, 0, 0, 0, 0,
- this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
- // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
- // divide by the pixel ratio
- let editorRect = new Rect(editorBounds.left / window.devicePixelRatio,
- editorBounds.top / window.devicePixelRatio,
- editorBounds.width / window.devicePixelRatio,
- editorBounds.height / window.devicePixelRatio);
-
- // Use intersection of the text rect and the editor rect
- let rect = new Rect(textBounds.left, textBounds.top, textBounds.width, textBounds.height);
- rect.restrictTo(editorRect);
-
- // Clamp vertically and scroll if handle is at bounds. The top and bottom
- // must be restricted by an additional pixel since clicking on the top
- // edge of an input field moves the cursor to the beginning of that
- // field's text (and clicking the bottom moves the cursor to the end).
- if (aY < rect.y + 1) {
- aY = rect.y + 1;
- this._getSelectionController().scrollLine(false);
- } else if (aY > rect.y + rect.height - 1) {
- aY = rect.y + rect.height - 1;
- this._getSelectionController().scrollLine(true);
- }
-
- // Clamp horizontally and scroll if handle is at bounds
- if (aX < rect.x) {
- aX = rect.x;
- this._getSelectionController().scrollCharacter(false);
- } else if (aX > rect.x + rect.width) {
- aX = rect.x + rect.width;
- this._getSelectionController().scrollCharacter(true);
- }
-
- this._domWinUtils.sendMouseEventToWindow("mousedown", aX, aY, 0, 0, 0, true);
- this._domWinUtils.sendMouseEventToWindow("mouseup", aX, aY, 0, 0, 0, true);
- },
-
- copySelection: function sh_copySelection() {
- let selectedText = this._getSelectedText();
- if (selectedText.length) {
- let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
- clipboard.copyString(selectedText);
- Snackbars.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), Snackbars.LENGTH_LONG);
- }
- this._closeSelection();
- },
-
- shareSelection: function sh_shareSelection() {
- let selectedText = this._getSelectedText();
- if (selectedText.length) {
- Messaging.sendRequest({
- type: "Share:Text",
- text: selectedText
- });
- }
- this._closeSelection();
- },
-
- searchSelection: function sh_searchSelection() {
- let selectedText = this._getSelectedText();
- if (selectedText.length) {
- let req = Services.search.defaultEngine.getSubmission(selectedText);
- let parent = BrowserApp.selectedTab;
- let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
- // Set current tab as parent of new tab, and set new tab as private if the parent is.
- BrowserApp.addTab(req.uri.spec, {parentId: parent.id,
- selected: true,
- isPrivate: isPrivate});
- }
- this._closeSelection();
- },
-
- _phoneRegex: /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/,
-
- _getSelectedPhoneNumber: function sh_getSelectedPhoneNumber() {
- return this._isPhoneNumber(this._getSelectedText().trim());
- },
-
- _isPhoneNumber: function sh_isPhoneNumber(selectedText) {
- return (this._phoneRegex.test(selectedText) ? selectedText : null);
- },
-
- callSelection: function sh_callSelection() {
- let selectedText = this._getSelectedPhoneNumber();
- if (selectedText) {
- BrowserApp.loadURI("tel:" + selectedText);
- }
- this._closeSelection();
- },
-
- /**
- * Deferred _closeSelection() actions allow for brief periods where programmatic
- * selection changes have effectively closed the selection, but we anticipate further
- * activity that may restore it.
- *
- * At this point, we hide the UI handles, and stop responding to messages until
- * either the final _closeSelection() is triggered, or until our Gecko selectionListener
- * notices a subsequent programmatic selection that results in a new selection.
- */
- _deferCloseSelection: function() {
- // Schedule the deferred _closeSelection() action.
- this._deferCloseTimer = setTimeout((function() {
- // Time is up! Close the selection.
- this._deferCloseTimer = null;
- this._closeSelection();
- }).bind(this), DEFER_CLOSE_TRIGGER_MS);
-
- // Hide any handles while deferClosed.
- if (this._prevHandlePositions.length) {
- let positions = this._prevHandlePositions;
- for (let i in positions) {
- positions[i].hidden = true;
- }
-
- Messaging.sendRequest({
- type: "TextSelection:PositionHandles",
- positions: positions,
- });
- }
- },
-
- /**
- * Cancel any current deferred _closeSelection() action.
- */
- _cancelDeferredCloseSelection: function() {
- if (this._deferCloseTimer) {
- clearTimeout(this._deferCloseTimer);
- this._deferCloseTimer = null;
- }
- },
-
- /*
- * Shuts SelectionHandler down.
- */
- _closeSelection: function sh_closeSelection() {
- // Bail if there's no active selection
- if (this._activeType == this.TYPE_NONE)
- return;
-
- if (this._activeType == this.TYPE_SELECTION)
- this._clearSelection();
-
- this._deactivate();
- },
-
- _clearSelection: function sh_clearSelection() {
- // Cancel any in-progress / deferred _closeSelection() process.
- this._cancelDeferredCloseSelection();
-
- let selection = this._getSelection();
- if (selection) {
- // Remove our listener before we clear the selection
- this._removeSelectionListener();
-
- // Remove the selection. For editables, we clear selection without losing
- // element focus. For non-editables, just clear all.
- if (selection.rangeCount != 0) {
- if (this.isElementEditableText(this._targetElement)) {
- selection.collapseToStart();
- } else {
- selection.removeAllRanges();
- }
- }
- }
- },
-
- _deactivate: function sh_deactivate() {
- this._stopDraggingHandles();
- // Hide handle/caret, close actionbar
- Messaging.sendRequest({ type: "TextSelection:HideHandles" });
-
- this._removeObservers();
-
- // Only observed for caret positioning
- if (this._activeType == this.TYPE_CURSOR) {
- BrowserApp.deck.removeEventListener("keyup", this);
- BrowserApp.deck.removeEventListener("compositionupdate", this);
- BrowserApp.deck.removeEventListener("compositionend", this);
- }
-
- this._contentWindow = null;
- this._targetElement = null;
- this._targetDOMCaretNode = null;
-
- this._targetIsRTL = false;
- this._ignoreCompositionChanges = false;
- this._prevHandlePositions = [];
- this._prevTargetElementHasText = null;
-
- this._activeType = this.TYPE_NONE;
- },
-
- _getViewOffset: function sh_getViewOffset() {
- let offset = { x: 0, y: 0 };
- let win = this._contentWindow;
-
- // Recursively look through frames to compute the total position offset.
- while (win.frameElement) {
- let rect = win.frameElement.getBoundingClientRect();
- offset.x += rect.left;
- offset.y += rect.top;
-
- win = win.parent;
- }
-
- return offset;
- },
-
- /*
- * The direction of the Selection is ensured for editables while the user drags
- * the handles (per "TextSelection:Move" event). For non-editables, we just let
- * the user change direction, but fix it up at the end of handle movement (final
- * "TextSelection:Position" event).
- */
- _ensureSelectionDirection: function() {
- // Never needed at this time.
- if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
- return;
- }
-
- // Nothing needed if not reversed.
- let qcEventResult = this._domWinUtils.sendQueryContentEvent(
- this._domWinUtils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
- if (!qcEventResult.reversed) {
- return;
- }
-
- // Reverse the Selection.
- let selection = this._getSelection();
- let newFocusNode = selection.anchorNode;
- let newFocusOffset = selection.anchorOffset;
-
- selection.collapse(selection.focusNode, selection.focusOffset);
- selection.extend(newFocusNode, newFocusOffset);
- },
-
- /*
- * Updates the TYPE_SELECTION cache, with the handle anchor/focus point values
- * of the current selection. Passed to Java for UI positioning only.
- *
- * Note that the anchor handle and focus handle can reference text in nodes
- * with mixed direction. (ie a.direction = "rtl" while f.direction = "ltr").
- */
- _updateCacheForSelection: function() {
- let selection = this._getSelection();
- let rects = selection.getRangeAt(0).getClientRects();
- if (rects.length == 0) {
- // nsISelection object exists, but there's nothing actually selected
- throw "Failed to update cache for invalid selection";
- }
-
- // Right-to-Left (ie: Hebrew) anchorPt is on right,
- // Left-to-Right (ie: English) anchorPt is on left.
- this._anchorIsRTL = this._isNodeRTL(selection.anchorNode);
- let anchorIdx = 0;
- this._cache.anchorPt = (this._anchorIsRTL) ?
- new Point(rects[anchorIdx].right, rects[anchorIdx].bottom) :
- new Point(rects[anchorIdx].left, rects[anchorIdx].bottom);
-
- // Right-to-Left (ie: Hebrew) focusPt is on left,
- // Left-to-Right (ie: English) focusPt is on right.
- this._focusIsRTL = this._isNodeRTL(selection.focusNode);
- let focusIdx = rects.length - 1;
- this._cache.focusPt = (this._focusIsRTL) ?
- new Point(rects[focusIdx].left, rects[focusIdx].bottom) :
- new Point(rects[focusIdx].right, rects[focusIdx].bottom);
- },
-
- /*
- * Return true if text associated with a node is RTL.
- */
- _isNodeRTL: function(node) {
- // Find containing node that supports .direction attribute (needed
- // when target node is #text for example).
- while (node && !(node instanceof Element)) {
- node = node.parentNode;
- }
-
- // Worst case, use original direction from _targetElement.
- if (!node) {
- return this._targetIsRTL;
- }
-
- let nodeWin = node.ownerDocument.defaultView;
- let nodeStyle = nodeWin.getComputedStyle(node, "");
- return (nodeStyle.direction == "rtl");
- },
-
- _getHandlePositions: function(scroll = this._getScrollPos()) {
- // the checkHidden function tests to see if the given point is hidden inside an
- // iframe/subdocument. this is so that if we select some text inside an iframe and
- // scroll the iframe so the selection is out of view, we hide the handles rather
- // than having them float on top of the main page content.
- let checkHidden = function(x, y) {
- return false;
- };
- if (this._contentWindow.frameElement) {
- let bounds = this._contentWindow.frameElement.getBoundingClientRect();
- checkHidden = function(x, y) {
- return x < 0 || y < 0 || x > bounds.width || y > bounds.height;
- };
- }
-
- if (this._activeType == this.TYPE_CURSOR) {
- // The left and top properties returned are relative to the client area
- // of the window, so we don't need to account for a sub-frame offset.
- let cursor = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_CARET_RECT, this._targetElement.selectionEnd, 0, 0, 0,
- this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
- // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
- // divide by the pixel ratio
- let x = cursor.left / window.devicePixelRatio;
- let y = (cursor.top + cursor.height) / window.devicePixelRatio;
- return [{ handle: this.HANDLE_TYPE_CARET,
- left: x + scroll.X,
- top: y + scroll.Y,
- rtl: this._targetIsRTL,
- hidden: checkHidden(x, y) }];
- }
-
- // Determine the handle screen coords
- this._updateCacheForSelection();
- let offset = this._getViewOffset();
- return [{ handle: this.HANDLE_TYPE_ANCHOR,
- left: this._cache.anchorPt.x + offset.x + scroll.X,
- top: this._cache.anchorPt.y + offset.y + scroll.Y,
- rtl: this._anchorIsRTL,
- hidden: checkHidden(this._cache.anchorPt.x, this._cache.anchorPt.y) },
- { handle: this.HANDLE_TYPE_FOCUS,
- left: this._cache.focusPt.x + offset.x + scroll.X,
- top: this._cache.focusPt.y + offset.y + scroll.Y,
- rtl: this._focusIsRTL,
- hidden: checkHidden(this._cache.focusPt.x, this._cache.focusPt.y) }];
- },
-
- // Position handles, but avoid superfluous re-positioning (helps during
- // "TextSelection:LayerReflow", "scroll" of top-level document, etc).
- _positionHandlesOnChange: function() {
- // Helper function to compare position messages
- let samePositions = function(aPrev, aCurr) {
- if (aPrev.length != aCurr.length) {
- return false;
- }
- for (let i = 0; i < aPrev.length; i++) {
- if (aPrev[i].left != aCurr[i].left ||
- aPrev[i].top != aCurr[i].top ||
- aPrev[i].rtl != aCurr[i].rtl ||
- aPrev[i].hidden != aCurr[i].hidden) {
- return false;
- }
- }
- return true;
- }
-
- let positions = this._getHandlePositions();
- if (!samePositions(this._prevHandlePositions, positions)) {
- this._positionHandles(positions);
- }
- },
-
- // Position handles, allow for re-position, in case user drags handle
- // to invalid position, then releases, we can put it back where it started
- // positions is an array of objects with data about handle positions,
- // which we get from _getHandlePositions.
- _positionHandles: function(positions = this._getHandlePositions()) {
- Messaging.sendRequest({
- type: "TextSelection:PositionHandles",
- positions: positions,
- });
- this._prevHandlePositions = positions;
-
- // Text state transitions (text <--> no text) will affect selection context and actionbar display
- let currTargetElementHasText = (this._targetElement.textLength > 0);
- if (currTargetElementHasText != this._prevTargetElementHasText) {
- this._prevTargetElementHasText = currTargetElementHasText;
- this._updateMenu();
- }
- },
-
- subdocumentScrolled: function sh_subdocumentScrolled(aElement) {
- // Ignore all but selectionListener notifications during deferred _closeSelection().
- if (this._deferCloseTimer) {
- return;
- }
-
- if (this._activeType == this.TYPE_NONE) {
- return;
- }
- let scrollView = aElement.ownerDocument.defaultView;
- let view = this._contentWindow;
- while (true) {
- if (view == scrollView) {
- // The selection is in a view (or sub-view) of the view that scrolled.
- // So we need to reposition the handles.
- this._positionHandles();
- break;
- }
- if (view == view.parent) {
- break;
- }
- view = view.parent;
- }
- }
-};
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -149,17 +149,16 @@ lazilyLoadedBrowserScripts.forEach(funct
var lazilyLoadedObserverScripts = [
["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
- ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
];
lazilyLoadedObserverScripts.push(
["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
"chrome://browser/content/ActionBarHandler.js"]
@@ -2682,38 +2681,16 @@ var NativeWindow = {
return;
}
// If no context-menu for long-press event, it may be meant to trigger text-selection.
this.menus = null;
Services.obs.notifyObservers(
{target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
-
- if (SelectionHandler.canSelect(this._target)) {
- // If textSelection WORD is successful,
- // consume / preventDefault the context menu event.
- let selectionResult = SelectionHandler.startSelection(this._target,
- { mode: SelectionHandler.SELECT_AT_POINT,
- x: event.clientX,
- y: event.clientY
- }
- );
- if (selectionResult === SelectionHandler.ERROR_NONE) {
- event.preventDefault();
- return;
- }
-
- // If textSelection caret-attachment is successful,
- // consume / preventDefault the context menu event.
- if (SelectionHandler.attachCaret(this._target) === SelectionHandler.ERROR_NONE) {
- event.preventDefault();
- return;
- }
- }
},
// Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
_getTitle: function(node) {
if (node.hasAttribute && node.hasAttribute("title")) {
return node.getAttribute("title");
}
return this._getUrl(node);
@@ -4609,31 +4586,17 @@ var BrowserEventHandler = {
handleUserEvent: function(aTopic, aData) {
switch (aTopic) {
case "Gesture:ClickInZoomedView":
this._clickInZoomedView = true;
break;
case "Gesture:SingleTap": {
- let focusedElement = null;
- try {
- // If the element was previously focused, show the caret attached to it.
- let element = this._highlightElement;
- focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
- if (element && element == focusedElement) {
- let result = SelectionHandler.attachCaret(element);
- if (result !== SelectionHandler.ERROR_NONE) {
- dump("Unexpected failure during caret attach: " + result);
- }
- }
- } catch(e) {
- Cu.reportError(e);
- }
-
+ let focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
let data = JSON.parse(aData);
let {x, y} = data;
if (this._inCluster && this._clickInZoomedView != true) {
// If there is a focused element, the display of the zoomed view won't remove the focus.
// In this case, the form assistant linked to the focused element will never be closed.
// To avoid this situation, the focus is moved and the form assistant is closed.
if (focusedElement) {
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -28,17 +28,16 @@ chrome.jar:
content/browser.css (content/browser.css)
content/browser.js (content/browser.js)
content/geckoview.xul (content/geckoview.xul)
content/geckoview.js (content/geckoview.js)
content/bindings/checkbox.xml (content/bindings/checkbox.xml)
content/bindings/settings.xml (content/bindings/settings.xml)
content/netError.xhtml (content/netError.xhtml)
content/SelectHelper.js (content/SelectHelper.js)
- content/SelectionHandler.js (content/SelectionHandler.js)
content/ActionBarHandler.js (content/ActionBarHandler.js)
content/EmbedRT.js (content/EmbedRT.js)
content/InputWidgetHelper.js (content/InputWidgetHelper.js)
content/WebrtcUI.js (content/WebrtcUI.js)
content/MemoryObserver.js (content/MemoryObserver.js)
content/ConsoleAPI.js (content/ConsoleAPI.js)
content/PluginHelper.js (content/PluginHelper.js)
content/PrintHelper.js (content/PrintHelper.js)
--- a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
@@ -30,26 +30,26 @@
"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
"unpack": true
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "jcentral.tar.xz",
"unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "gradle-dist.tar.xz",
"unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "dotgradle.tar.xz",
"unpack": true,
"digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
"size": 512
--- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
@@ -45,26 +45,26 @@
"filename": "gcc.tar.xz",
"unpack": true
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "jcentral.tar.xz",
"unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "gradle-dist.tar.xz",
"unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
},
{
"size": 30899096,
"visibility": "public",
"digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
"algorithm": "sha512",
"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
"unpack": true
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -55,26 +55,26 @@
"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
"unpack": true
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "jcentral.tar.xz",
"unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
},
{
"algorithm": "sha512",
"visibility": "public",
"filename": "gradle-dist.tar.xz",
"unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
},
{
"version": "rustc 1.11.0 (9b21dcd6a 2016-08-15) repack",
"size": 97552448,
"digest": "272438c1692a46998dc44f22bd1fe18da1be7af2e7fdcf6c52709366c80c73e30637f0c3864f45c64edf46ce6a905538c14b2313983be973f9f29a2f191ec89b",
"algorithm": "sha512",
"filename": "rustc.tar.xz",
"unpack": true
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,86 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ targetSdkVersion 23
+ minSdkVersion 15
+ }
+
+ buildTypes {
+ withGeckoBinaries {
+ initWith release
+ }
+ withoutGeckoBinaries { // For clarity and consistency throughout the tree.
+ initWith release
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+
+ dexOptions {
+ javaMaxHeapSize "2g"
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ sourceSets {
+ main {
+ java {
+ srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
+
+ // TODO: support WebRTC.
+ // if (mozconfig.substs.MOZ_WEBRTC) {
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+ // srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+ // }
+
+ // TODO: don't use AppConstants.
+ srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+ }
+
+ assets {
+ }
+ }
+ }
+}
+
+dependencies {
+ compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+ into("${project.buildDir}/generated/source/preprocessed_code")
+ from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+ // AdjustConstants is included in the main app project.
+ exclude '**/AdjustConstants.java'
+ }
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.libraryVariants.all { variant ->
+ variant.preBuild.dependsOn syncPreprocessedCode
+
+ // Like 'debug', 'release', or 'withGeckoBinaries'.
+ def buildType = variant.buildType.name
+
+ // It would be most natural for :geckoview to always include the Gecko
+ // binaries, but that's difficult; see the notes in
+ // mobile/android/gradle/with_gecko_binaries.gradle. Instead :app uses
+ // :geckoview:release and handles it's own Gecko binary inclusion.
+ if (buildType.equals('withGeckoBinaries')) {
+ configureVariantWithGeckoBinaries(variant)
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.geckoview">
+
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
+ 19+. We declare it so that the bouncer APK and the main APK have the
+ same set of permissions. -->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
+
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+
+ <uses-feature android:name="android.hardware.location" android:required="false"/>
+ <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
+ <uses-feature android:name="android.hardware.touchscreen"/>
+
+ <!--#ifdef MOZ_WEBRTC-->
+ <!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
+ <!--<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>-->
+ <!--<uses-feature android:name="android.hardware.camera.any" android:required="false"/>-->
+ <!--<uses-feature android:name="android.hardware.microphone" android:required="false"/>-->
+ <!--#endif-->
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" android:required="false"/>
+ <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
+
+ <!-- App requires OpenGL ES 2.0 -->
+ <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+</manifest>
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -690,47 +690,16 @@ class GeckoLayerClient implements LayerV
public void panZoomStopped() {
mToolbarAnimator.onPanZoomStopped();
}
Object getLock() {
return this;
}
- /**
- * Converts a point from layer view coordinates to layer coordinates. In other words, given a
- * point measured in pixels from the top left corner of the layer view, returns the point in
- * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
- * events being sent to Gecko are processed in FIFO order, this calculation should always be
- * correct.
- */
- PointF convertViewPointToLayerPoint(PointF viewPoint) {
- if (!mGeckoIsReady) {
- return null;
- }
-
- ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
- PointF origin = viewportMetrics.getOrigin();
- float zoom = viewportMetrics.zoomFactor;
- ImmutableViewportMetrics geckoViewport = mViewportMetrics;
- PointF geckoOrigin = geckoViewport.getOrigin();
- float geckoZoom = geckoViewport.zoomFactor;
-
- // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
- // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
- // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
- // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
- // the current Gecko coordinate in CSS pixels.
- PointF layerPoint = new PointF(
- ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
- ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
-
- return layerPoint;
- }
-
Matrix getMatrixForLayerRectToViewRect() {
if (!mGeckoIsReady) {
return null;
}
ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
PointF origin = viewportMetrics.getOrigin();
float zoom = viewportMetrics.zoomFactor;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -345,20 +345,16 @@ public class LayerView extends FrameLayo
public PanZoomController getPanZoomController() { return mPanZoomController; }
public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
public ImmutableViewportMetrics getViewportMetrics() {
return mLayerClient.getViewportMetrics();
}
- public PointF convertViewPointToLayerPoint(PointF viewPoint) {
- return mLayerClient.convertViewPointToLayerPoint(viewPoint);
- }
-
public Matrix getMatrixForLayerRectToViewRect() {
return mLayerClient.getMatrixForLayerRectToViewRect();
}
public void setSurfaceBackgroundColor(int newColor) {
if (mSurfaceView != null) {
mSurfaceView.setBackgroundColor(newColor);
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/build.gradle
@@ -0,0 +1,62 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+ defaultConfig {
+ applicationId "org.mozilla.geckoview_example"
+ minSdkVersion 15
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ // This is extremely frustrating, but the only way to do it automation for
+ // now. Without this, we only get a "debugAndroidTest" configuration; we
+ // have no "withoutGeckoBinariesAndroidTest" configuration.
+ testBuildType "withoutGeckoBinaries"
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ withGeckoBinaries { // For consistency with :geckoview project in Task Cluster invocations.
+ initWith debug
+ }
+ withoutGeckoBinaries { // Logical negation of withGeckoBinaries.
+ initWith debug
+ }
+ }
+}
+
+dependencies {
+ testCompile 'junit:junit:4.12'
+
+ compile 'com.android.support:support-annotations:23.0.1'
+
+ // Later versions (2.2.2, 0.5) requires newer support libraries, leading to
+ // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.0.1) and test app (23.1.1) differ."
+ androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
+ androidTestCompile 'com.android.support.test:runner:0.4.1'
+
+ compile project(':geckoview')
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.applicationVariants.all { variant ->
+ // Like 'debug', 'release', or 'withoutGeckoBinaries'.
+ def buildType = variant.buildType.name
+
+ // It would be most natural for :geckoview to always include the Gecko
+ // binaries, but that's difficult; see the notes in
+ // mobile/android/gradle/with_gecko_binaries.gradle. Instead we handle our
+ // own Gecko binary inclusion.
+ if (!buildType.equals('withoutGeckoBinaries')) {
+ configureVariantWithGeckoBinaries(variant)
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/nalexander/.mozbuild/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.mozilla.geckoview_example;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java
@@ -0,0 +1,32 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview_example;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+@RunWith(AndroidJUnit4.class)
+public class GeckoViewActivityTest {
+
+ @Rule
+ public ActivityTestRule<GeckoViewActivity> mActivityRule = new ActivityTestRule(GeckoViewActivity.class);
+
+ @Test
+ public void testA() throws InterruptedException {
+ onView(withId(R.id.gecko_view))
+ .check(matches(isDisplayed()));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.mozilla.geckoview_example">
+
+ <application android:allowBackup="true"
+ android:label="@string/app_name"
+ android:supportsRtl="true">
+
+ <uses-library android:name="android.test.runner" />
+
+ <activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
+ android:label="GeckoViewActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -0,0 +1,142 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview_example;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoView;
+import org.mozilla.gecko.PrefsHelper;
+
+public class GeckoViewActivity extends Activity {
+ private static final String LOGTAG = "GeckoViewActivity";
+
+ GeckoView mGeckoView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.geckoview_activity);
+
+ mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
+ mGeckoView.setChromeDelegate(new MyGeckoViewChrome());
+ mGeckoView.setContentDelegate(new MyGeckoViewContent());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ final GeckoProfile profile = GeckoProfile.get(getApplicationContext());
+
+ GeckoThread.init(profile, /* args */ null, /* action */ null, /* debugging */ false);
+ GeckoThread.launch();
+ }
+
+ private class MyGeckoViewChrome implements GeckoView.ChromeDelegate {
+ @Override
+ public void onReady(GeckoView view) {
+ Log.i(LOGTAG, "Gecko is ready");
+ // // Inject a script that adds some code to the content window
+ // mGeckoView.importScript("resource://android/assets/script.js");
+
+ // Set up remote debugging to a port number
+ PrefsHelper.setPref("layers.dump", true);
+ PrefsHelper.setPref("devtools.debugger.remote-port", 6000);
+ PrefsHelper.setPref("devtools.debugger.unix-domain-socket", "");
+ PrefsHelper.setPref("devtools.debugger.remote-enabled", true);
+
+ // The Gecko libraries have finished loading and we can use the rendering engine.
+ // Let's add a browser (required) and load a page into it.
+ // mGeckoView.addBrowser(getResources().getString(R.string.default_url));
+ }
+
+ @Override
+ public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+ Log.i(LOGTAG, "Alert!");
+ result.confirm();
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, final GeckoView.PromptResult result) {
+ Log.i(LOGTAG, "Confirm!");
+ new AlertDialog.Builder(GeckoViewActivity.this)
+ .setTitle("javaScript dialog")
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ result.cancel();
+ }
+ })
+ .create()
+ .show();
+ }
+
+ @Override
+ public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+ result.cancel();
+ }
+
+ @Override
+ public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+ Log.i(LOGTAG, "Remote Debug!");
+ result.confirm();
+ }
+
+ @Override
+ public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
+ Log.i(LOGTAG, "Got Script Message: " + data.toString());
+ String type = data.getString("type");
+ if ("fetch".equals(type)) {
+ Bundle ret = new Bundle();
+ ret.putString("name", "Mozilla");
+ ret.putString("url", "https://mozilla.org");
+ result.success(ret);
+ }
+ }
+ }
+
+ private class MyGeckoViewContent implements GeckoView.ContentDelegate {
+ @Override
+ public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {
+
+ }
+
+ @Override
+ public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {
+
+ }
+
+ @Override
+ public void onPageShow(GeckoView view, GeckoView.Browser browser) {
+
+ }
+
+ @Override
+ public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {
+ Log.i(LOGTAG, "Received a title: " + title);
+ }
+
+ @Override
+ public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {
+ Log.i(LOGTAG, "Received a favicon URL: " + url);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+
+ <org.mozilla.gecko.GeckoView
+ android:id="@+id/gecko_view"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:scrollbars="none"
+ />
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">geckoview_example</string>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package org.mozilla.geckoview_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/with_gecko_binaries.gradle
@@ -0,0 +1,105 @@
+// We run fairly hard into a fundamental limitation of the Android Gradle
+// plugin. There are many bugs filed about this, but
+// https://code.google.com/p/android/issues/detail?id=216978#c6 is a reason one.
+// The issue is that we need fine-grained control over when to include Gecko's
+// binary libraries into the GeckoView AAR and the Fennec APK, and that's hard
+// to achieve. In particular:
+//
+// * :app:automation wants :geckoview to not include Gecko binaries (automation
+// * build, before package)
+//
+// * :geckoview:withLibraries wants :geckoview to include Gecko binaries
+// * (automation build, after package)
+//
+// * non-:app:automation wants :geckoview to include Gecko binaries (local
+// * build, always after package)
+//
+// publishNonDefault (see
+// http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Library-Publication)
+// is intended to address this, but doesn't handle our case. That option always
+// builds *all* configurations, which fails when the required Gecko binaries
+// don't exist (automation build, before package). So instead, we make both
+// :app and :geckoview both know how to include the Gecko binaries, and use a
+// non-default, non-published :geckoview:withGeckoBinaries configuration to
+// handle automation's needs. Simple, right?
+
+// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
+// That arrangement labels them nicely in IntelliJ. See the comment in the
+// :omnijar project for more context.
+evaluationDependsOn(':omnijar')
+
+task buildOmnijar(type:Exec) {
+ dependsOn rootProject.generateCodeAndResources
+
+ // See comment in :omnijar project regarding interface mismatches here.
+ inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
+
+ // Produce a single output file.
+ outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+
+ workingDir "${topobjdir}"
+
+ commandLine mozconfig.substs.GMAKE
+ args '-C'
+ args "${topobjdir}/mobile/android/base"
+ args 'gradle-omnijar'
+
+ // Only show the output if something went wrong.
+ ignoreExitValue = true
+ standardOutput = new ByteArrayOutputStream()
+ errorOutput = standardOutput
+ doLast {
+ if (execResult.exitValue != 0) {
+ throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+ }
+ }
+}
+
+task syncOmnijarFromDistDir(type: Sync) {
+ into("${project.buildDir}/generated/omnijar")
+ from("${topobjdir}/dist/fennec/assets") {
+ include 'omni.ja'
+ }
+}
+
+task checkLibsExistInDistDir<< {
+ if (syncLibsFromDistDir.source.empty) {
+ throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib. Have you built and packaged?")
+ }
+}
+
+task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
+ into("${project.buildDir}/generated/jniLibs")
+ from("${topobjdir}/dist/fennec/lib")
+}
+
+task checkAssetsExistInDistDir<< {
+ if (syncAssetsFromDistDir.source.empty) {
+ throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets. Have you built and packaged?")
+ }
+}
+
+task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
+ into("${project.buildDir}/generated/assets")
+ from("${topobjdir}/dist/fennec/assets") {
+ exclude 'omni.ja'
+ }
+}
+
+ext.configureVariantWithGeckoBinaries = { variant ->
+ // Like 'local' or 'localOld'; may be null.
+ def productFlavor = variant.productFlavors ? variant.productFlavors[0].name : ""
+ // Like 'debug' or 'release'.
+ def buildType = variant.buildType.name
+
+ syncOmnijarFromDistDir.dependsOn buildOmnijar
+ def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
+ generateAssetsTask.dependsOn syncOmnijarFromDistDir
+ generateAssetsTask.dependsOn syncLibsFromDistDir
+ generateAssetsTask.dependsOn syncAssetsFromDistDir
+
+ def sourceSet = productFlavor ? "${productFlavor}${buildType.capitalize()}" : buildType
+ android.sourceSets."${sourceSet}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+ android.sourceSets."${sourceSet}".assets.srcDir syncAssetsFromDistDir.destinationDir
+ android.sourceSets."${sourceSet}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+}
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java
@@ -76,17 +76,34 @@ public class TestHomeConfigPrefsBackendM
},
new PanelType[] {
// Last version: no migration exists yet, we only need to define a list
// of expected panels.
}
));
}
- private JSONArray createConfigsForList(Context context, PanelType[] panels, int defaultIndex) throws JSONException {
+ private JSONArray createDisabledConfigsForList(Context context,
+ PanelType[] panels) throws JSONException {
+ final JSONArray jsonPanels = new JSONArray();
+
+ for (int i = 0; i < panels.length; i++) {
+ final PanelType panel = panels[i];
+
+ jsonPanels.put(HomeConfig.createBuiltinPanelConfig(context, panel,
+ EnumSet.of(PanelConfig.Flags.DISABLED_PANEL)).toJSON());
+ }
+
+ return jsonPanels;
+
+ }
+
+
+ private JSONArray createConfigsForList(Context context, PanelType[] panels,
+ int defaultIndex) throws JSONException {
if (defaultIndex < 0 || defaultIndex >= panels.length) {
throw new IllegalArgumentException("defaultIndex must point to panel in the array");
}
final JSONArray jsonPanels = new JSONArray();
for (int i = 0; i < panels.length; i++) {
final PanelType panel = panels[i];
@@ -115,17 +132,27 @@ public class TestHomeConfigPrefsBackendM
if (panelConfig.isDefault()) {
return panelConfig.getType();
}
}
return null;
}
- private void checkListContainsExpectedPanels(JSONArray jsonPanels, PanelType[] expected) throws JSONException {
+ private void checkAllPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
+ for (int i = 0; i < jsonPanels.length(); i++) {
+ final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
+ final PanelConfig config = new PanelConfig(jsonPanelConfig);
+
+ assertTrue("Non disabled panel \"" + config.getType().name() + "\" found in list, excpected all panels to be disabled", config.isDisabled());
+ }
+ }
+
+ private void checkListContainsExpectedPanels(JSONArray jsonPanels,
+ PanelType[] expected) throws JSONException {
// Given the short lists we have here an ArraySet might be more appropriate, but it requires API >= 23.
final Set<PanelType> expectedSet = new HashSet<>();
for (PanelType panelType : expected) {
expectedSet.add(panelType);
}
for (int i = 0; i < jsonPanels.length(); i++) {
final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
@@ -147,17 +174,17 @@ public class TestHomeConfigPrefsBackendM
final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION);
assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations",
finalConstellation);
// We want to calculate the number of iterations here to make sure we cover all provided constellations.
// Iterating over the array and manually checking for each version could result in constellations
// being skipped if there are any gaps in the array
- final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
+ final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
// The last constellation is only used for the counts / expected outputs, hence we start
// with the second-last constellation
for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) {
final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion);
assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list",
currentConstellation);
@@ -191,9 +218,47 @@ public class TestHomeConfigPrefsBackendM
assertEquals("Number of panels after migration doesn't match expected count",
jsonPanels.length(), expectedOutputList.length);
checkListContainsExpectedPanels(jsonPanels, expectedOutputList);
}
}
}
+
+ // Test that if all panels are disabled, the migration retains all panels as being disabled
+ // (in addition to correctly removing panels as necessary).
+ @Test
+ public void testMigrationRetainsAllPanelsHiddenAfter6() throws JSONException {
+ final Context context = RuntimeEnvironment.application;
+
+ final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION);
+ assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations",
+ finalConstellation);
+
+ final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
+
+ for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) {
+ final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion);
+ assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list",
+ currentConstellation);
+
+ final PanelType[] inputList = currentConstellation.first;
+
+ JSONArray jsonPanels = createDisabledConfigsForList(context, inputList);
+
+ jsonPanels = HomeConfigPrefsBackend.migratePrefsFromVersionToVersion(context, testVersion, testVersion + 1, jsonPanels, null);
+
+ // All panels should remain disabled after the migration
+ checkAllPanelsAreDisabled(jsonPanels);
+
+ // Duplicated from previous test:
+ // Verify that the panels remaining after the migration correspond to the input panels
+ // for the next migration
+ final PanelType[] expectedOutputList = migrationConstellations.get(testVersion + 1).first;
+
+ assertEquals("Number of panels after migration doesn't match expected count",
+ jsonPanels.length(), expectedOutputList.length);
+
+ checkListContainsExpectedPanels(jsonPanels, expectedOutputList);
+ }
+ }
}
--- a/mobile/android/thirdparty/build.gradle
+++ b/mobile/android/thirdparty/build.gradle
@@ -20,17 +20,16 @@ android {
abortOnError false
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java {
srcDir '.'
- srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
exclude 'com/adjust/**'
}
// Exclude LeakCanary: It will be added again via a gradle dependency. This version
// here is only the no-op library for mach-based builds.
exclude 'com/squareup/leakcanary/**'
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -417,16 +417,18 @@ pref("media.navigator.load_adapt.high_lo
pref("media.navigator.load_adapt.low_load","0.40");
pref("media.navigator.video.default_fps",30);
pref("media.navigator.video.default_minfps",10);
pref("media.navigator.video.use_remb", true);
pref("media.navigator.video.use_tmmbr", false);
pref("media.navigator.audio.use_fec", true);
pref("media.navigator.video.red_ulpfec_enabled", false);
+pref("media.peerconnection.dtmf.enabled", false);
+
pref("media.webrtc.debug.trace_mask", 0);
pref("media.webrtc.debug.multi_log", false);
pref("media.webrtc.debug.aec_log_dir", "");
pref("media.webrtc.debug.log_file", "");
pref("media.webrtc.debug.aec_dump_max_size", 4194304); // 4MB
#ifdef MOZ_WIDGET_GONK
pref("media.navigator.video.default_width", 320);
--- a/security/sandbox/linux/SandboxBrokerClient.cpp
+++ b/security/sandbox/linux/SandboxBrokerClient.cpp
@@ -30,23 +30,25 @@ SandboxBrokerClient::SandboxBrokerClient
SandboxBrokerClient::~SandboxBrokerClient()
{
close(mFileDesc);
}
int
SandboxBrokerClient::DoCall(const Request* aReq, const char* aPath,
- struct stat* aStat, bool expectFd)
+ const char* aPath2, void* aResponseBuff,
+ bool expectFd)
{
// Remap /proc/self to the actual pid, so that the broker can open
// it. This happens here instead of in the broker to follow the
// principle of least privilege and keep the broker as simple as
// possible. (Note: when pid namespaces happen, this will also need
// to remap the inner pid to the outer pid.)
+ // We only remap the first path.
static const char kProcSelf[] = "/proc/self/";
static const size_t kProcSelfLen = sizeof(kProcSelf) - 1;
const char* path = aPath;
// This buffer just needs to be large enough for any such path that
// the policy would actually allow. sizeof("/proc/2147483647/") == 18.
char rewrittenPath[64];
if (strncmp(aPath, kProcSelf, kProcSelfLen) == 0) {
ssize_t len =
@@ -57,125 +59,189 @@ SandboxBrokerClient::DoCall(const Reques
SANDBOX_LOG_ERROR("rewriting %s -> %s", aPath, rewrittenPath);
}
path = rewrittenPath;
} else {
SANDBOX_LOG_ERROR("not rewriting unexpectedly long path %s", aPath);
}
}
- struct iovec ios[2];
+ struct iovec ios[3];
int respFds[2];
// Set up iovecs for request + path.
ios[0].iov_base = const_cast<Request*>(aReq);
ios[0].iov_len = sizeof(*aReq);
ios[1].iov_base = const_cast<char*>(path);
- ios[1].iov_len = strlen(path);
+ ios[1].iov_len = strlen(path) + 1;
+ if (aPath2 != nullptr) {
+ ios[2].iov_base = const_cast<char*>(aPath2);
+ ios[2].iov_len = strlen(aPath2) + 1;
+ } else {
+ ios[2].iov_base = 0;
+ ios[2].iov_len = 0;
+ }
if (ios[1].iov_len > kMaxPathLen) {
return -ENAMETOOLONG;
}
+ if (ios[2].iov_len > kMaxPathLen) {
+ return -ENAMETOOLONG;
+ }
// Create response socket and send request.
if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, respFds) < 0) {
return -errno;
}
- const ssize_t sent = SendWithFd(mFileDesc, ios, 2, respFds[1]);
+ const ssize_t sent = SendWithFd(mFileDesc, ios, 3, respFds[1]);
const int sendErrno = errno;
MOZ_ASSERT(sent < 0 ||
- static_cast<size_t>(sent) == ios[0].iov_len + ios[1].iov_len);
+ static_cast<size_t>(sent) == ios[0].iov_len
+ + ios[1].iov_len
+ + ios[2].iov_len);
close(respFds[1]);
if (sent < 0) {
close(respFds[0]);
return -sendErrno;
}
// Set up iovecs for response.
Response resp;
ios[0].iov_base = &resp;
ios[0].iov_len = sizeof(resp);
- if (aStat) {
- ios[1].iov_base = aStat;
- ios[1].iov_len = sizeof(*aStat);
+ if (aResponseBuff) {
+ ios[1].iov_base = aResponseBuff;
+ ios[1].iov_len = aReq->mBufSize;
} else {
ios[1].iov_base = nullptr;
ios[1].iov_len = 0;
}
// Wait for response and return appropriately.
int openedFd = -1;
- const ssize_t recvd = RecvWithFd(respFds[0], ios, aStat ? 2 : 1,
+ const ssize_t recvd = RecvWithFd(respFds[0], ios, aResponseBuff ? 2 : 1,
expectFd ? &openedFd : nullptr);
const int recvErrno = errno;
close(respFds[0]);
if (recvd < 0) {
return -recvErrno;
}
if (recvd == 0) {
SANDBOX_LOG_ERROR("Unexpected EOF, op %d flags 0%o path %s",
aReq->mOp, aReq->mFlags, path);
return -EIO;
}
- if (resp.mError != 0) {
- // If the operation fails, the return payload will be empty;
- // adjust the iov_len for the following assertion.
- ios[1].iov_len = 0;
- }
- MOZ_ASSERT(static_cast<size_t>(recvd) == ios[0].iov_len + ios[1].iov_len);
- if (resp.mError == 0) {
+ MOZ_ASSERT(static_cast<size_t>(recvd) <= ios[0].iov_len + ios[1].iov_len);
+ // Some calls such as readlink return a size if successful
+ if (resp.mError >= 0) {
// Success!
if (expectFd) {
MOZ_ASSERT(openedFd >= 0);
return openedFd;
}
- return 0;
+ return resp.mError;
}
if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
// Keep in mind that "rejected" files can include ones that don't
// actually exist, if it's something that's optional or part of a
// search path (e.g., shared libraries). In those cases, this
// error message is expected.
SANDBOX_LOG_ERROR("Rejected errno %d op %d flags 0%o path %s",
resp.mError, aReq->mOp, aReq->mFlags, path);
}
if (openedFd >= 0) {
close(openedFd);
}
- return -resp.mError;
+ return resp.mError;
}
int
SandboxBrokerClient::Open(const char* aPath, int aFlags)
{
- Request req = { SANDBOX_FILE_OPEN, aFlags };
- int maybeFd = DoCall(&req, aPath, nullptr, true);
+ Request req = { SANDBOX_FILE_OPEN, aFlags, 0 };
+ int maybeFd = DoCall(&req, aPath, nullptr, nullptr, true);
if (maybeFd >= 0) {
// NSPR has opinions about file flags. Fix O_CLOEXEC.
if ((aFlags & O_CLOEXEC) == 0) {
fcntl(maybeFd, F_SETFD, 0);
}
}
return maybeFd;
}
int
SandboxBrokerClient::Access(const char* aPath, int aMode)
{
- Request req = { SANDBOX_FILE_ACCESS, aMode };
- return DoCall(&req, aPath, nullptr, false);
+ Request req = { SANDBOX_FILE_ACCESS, aMode, 0 };
+ return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Stat(const char* aPath, statstruct* aStat)
+{
+ Request req = { SANDBOX_FILE_STAT, 0, sizeof(statstruct) };
+ return DoCall(&req, aPath, nullptr, (void*)aStat, false);
+}
+
+int
+SandboxBrokerClient::LStat(const char* aPath, statstruct* aStat)
+{
+ Request req = { SANDBOX_FILE_STAT, O_NOFOLLOW, sizeof(statstruct) };
+ return DoCall(&req, aPath, nullptr, (void*)aStat, false);
+}
+
+int
+SandboxBrokerClient::Chmod(const char* aPath, int aMode)
+{
+ Request req = {SANDBOX_FILE_CHMOD, aMode, 0};
+ return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Link(const char* aOldPath, const char* aNewPath)
+{
+ Request req = {SANDBOX_FILE_LINK, 0, 0};
+ return DoCall(&req, aOldPath, aNewPath, nullptr, false);
}
int
-SandboxBrokerClient::Stat(const char* aPath, struct stat* aStat)
+SandboxBrokerClient::Symlink(const char* aOldPath, const char* aNewPath)
{
- Request req = { SANDBOX_FILE_STAT, 0 };
- return DoCall(&req, aPath, aStat, false);
+ Request req = {SANDBOX_FILE_SYMLINK, 0, 0};
+ return DoCall(&req, aOldPath, aNewPath, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Rename(const char* aOldPath, const char* aNewPath)
+{
+ Request req = {SANDBOX_FILE_RENAME, 0, 0};
+ return DoCall(&req, aOldPath, aNewPath, nullptr, false);
}
int
-SandboxBrokerClient::LStat(const char* aPath, struct stat* aStat)
+SandboxBrokerClient::Mkdir(const char* aPath, int aMode)
+{
+ Request req = {SANDBOX_FILE_MKDIR, aMode, 0};
+ return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Unlink(const char* aPath)
{
- Request req = { SANDBOX_FILE_STAT, O_NOFOLLOW };
- return DoCall(&req, aPath, aStat, false);
+ Request req = {SANDBOX_FILE_UNLINK, 0, 0};
+ return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Rmdir(const char* aPath)
+{
+ Request req = {SANDBOX_FILE_RMDIR, 0, 0};
+ return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Readlink(const char* aPath, void* aBuff, size_t aSize)
+{
+ Request req = {SANDBOX_FILE_READLINK, 0, aSize};
+ return DoCall(&req, aPath, nullptr, aBuff, false);
}
} // namespace mozilla
--- a/security/sandbox/linux/SandboxBrokerClient.h
+++ b/security/sandbox/linux/SandboxBrokerClient.h
@@ -3,16 +3,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_SandboxBrokerClient_h
#define mozilla_SandboxBrokerClient_h
#include "broker/SandboxBrokerCommon.h"
+#include "broker/SandboxBrokerUtils.h"
#include "mozilla/Attributes.h"
// This is the client for the sandbox broker described in
// broker/SandboxBroker.h; its constructor takes the file descriptor
// returned by SandboxBroker::Create, passed to the child over IPC.
//
// The operations exposed here can be called from any thread and in
@@ -26,21 +27,32 @@ namespace mozilla {
class SandboxBrokerClient final : private SandboxBrokerCommon {
public:
explicit SandboxBrokerClient(int aFd);
~SandboxBrokerClient();
int Open(const char* aPath, int aFlags);
int Access(const char* aPath, int aMode);
- int Stat(const char* aPath, struct stat* aStat);
- int LStat(const char* aPath, struct stat* aStat);
+ int Stat(const char* aPath, statstruct* aStat);
+ int LStat(const char* aPath, statstruct* aStat);
+ int Chmod(const char* aPath, int aMode);
+ int Link(const char* aPath, const char* aPath2);
+ int Mkdir(const char* aPath, int aMode);
+ int Symlink(const char* aOldPath, const char* aNewPath);
+ int Rename(const char* aOldPath, const char* aNewPath);
+ int Unlink(const char* aPath);
+ int Rmdir(const char* aPath);
+ int Readlink(const char* aPath, void* aBuf, size_t aBufSize);
private:
int mFileDesc;
- int DoCall(const Request* aReq, const char* aPath, struct stat* aStat,
+ int DoCall(const Request* aReq,
+ const char* aPath,
+ const char* aPath2,
+ void *aReponseBuff,
bool expectFd);
};
} // namespace mozilla
#endif // mozilla_SandboxBrokerClient_h
--- a/security/sandbox/linux/SandboxFilter.cpp
+++ b/security/sandbox/linux/SandboxFilter.cpp
@@ -378,48 +378,103 @@ class ContentSandboxPolicy : public Sand
return BlockedSyscallTrap(aArgs, nullptr);
}
return broker->Access(path, mode);
}
static intptr_t StatTrap(ArgsRef aArgs, void* aux) {
auto broker = static_cast<SandboxBrokerClient*>(aux);
auto path = reinterpret_cast<const char*>(aArgs.args[0]);
- auto buf = reinterpret_cast<struct stat*>(aArgs.args[1]);
+ auto buf = reinterpret_cast<statstruct*>(aArgs.args[1]);
return broker->Stat(path, buf);
}
static intptr_t LStatTrap(ArgsRef aArgs, void* aux) {
auto broker = static_cast<SandboxBrokerClient*>(aux);
auto path = reinterpret_cast<const char*>(aArgs.args[0]);
- auto buf = reinterpret_cast<struct stat*>(aArgs.args[1]);
+ auto buf = reinterpret_cast<statstruct*>(aArgs.args[1]);
return broker->LStat(path, buf);
}
static intptr_t StatAtTrap(ArgsRef aArgs, void* aux) {
auto broker = static_cast<SandboxBrokerClient*>(aux);
auto fd = static_cast<int>(aArgs.args[0]);
auto path = reinterpret_cast<const char*>(aArgs.args[1]);
- auto buf = reinterpret_cast<struct stat*>(aArgs.args[2]);
+ auto buf = reinterpret_cast<statstruct*>(aArgs.args[2]);
auto flags = static_cast<int>(aArgs.args[3]);
if (fd != AT_FDCWD && path[0] != '/') {
SANDBOX_LOG_ERROR("unsupported fd-relative fstatat(%d, \"%s\", %p, %d)",
fd, path, buf, flags);
return BlockedSyscallTrap(aArgs, nullptr);
}
if ((flags & ~AT_SYMLINK_NOFOLLOW) != 0) {
SANDBOX_LOG_ERROR("unsupported flags %d in fstatat(%d, \"%s\", %p, %d)",
(flags & ~AT_SYMLINK_NOFOLLOW), fd, path, buf, flags);
return BlockedSyscallTrap(aArgs, nullptr);
}
return (flags & AT_SYMLINK_NOFOLLOW) == 0
? broker->Stat(path, buf)
: broker->LStat(path, buf);
}
+ static intptr_t ChmodTrap(ArgsRef aArgs, void* aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ auto mode = static_cast<mode_t>(aArgs.args[1]);
+ return broker->Chmod(path, mode);
+ }
+
+ static intptr_t LinkTrap(ArgsRef aArgs, void *aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
+ return broker->Link(path, path2);
+ }
+
+ static intptr_t SymlinkTrap(ArgsRef aArgs, void *aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
+ return broker->Symlink(path, path2);
+ }
+
+ static intptr_t RenameTrap(ArgsRef aArgs, void *aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
+ return broker->Rename(path, path2);
+ }
+
+ static intptr_t MkdirTrap(ArgsRef aArgs, void* aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ auto mode = static_cast<mode_t>(aArgs.args[1]);
+ return broker->Mkdir(path, mode);
+ }
+
+ static intptr_t RmdirTrap(ArgsRef aArgs, void* aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ return broker->Rmdir(path);
+ }
+
+ static intptr_t UnlinkTrap(ArgsRef aArgs, void* aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ return broker->Unlink(path);
+ }
+
+ static intptr_t ReadlinkTrap(ArgsRef aArgs, void* aux) {
+ auto broker = static_cast<SandboxBrokerClient*>(aux);
+ auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+ auto buf = reinterpret_cast<char*>(aArgs.args[1]);
+ auto size = static_cast<size_t>(aArgs.args[2]);
+ return broker->Readlink(path, buf, size);
+ }
+
static intptr_t GetPPidTrap(ArgsRef aArgs, void* aux) {
// In a pid namespace, getppid() will return 0. We will return 0 instead
// of the real parent pid to see what breaks when we introduce the
// pid namespace (Bug 1151624).
return 0;
}
public:
@@ -508,16 +563,32 @@ public:
case __NR_faccessat:
return Trap(AccessAtTrap, mBroker);
CASES_FOR_stat:
return Trap(StatTrap, mBroker);
CASES_FOR_lstat:
return Trap(LStatTrap, mBroker);
CASES_FOR_fstatat:
return Trap(StatAtTrap, mBroker);
+ case __NR_chmod:
+ return Trap(ChmodTrap, mBroker);
+ case __NR_link:
+ return Trap(LinkTrap, mBroker);
+ case __NR_mkdir:
+ return Trap(MkdirTrap, mBroker);
+ case __NR_symlink:
+ return Trap(SymlinkTrap, mBroker);
+ case __NR_rename:
+ return Trap(RenameTrap, mBroker);
+ case __NR_rmdir:
+ return Trap(RmdirTrap, mBroker);
+ case __NR_unlink:
+ return Trap(UnlinkTrap, mBroker);
+ case __NR_readlink:
+ return Trap(ReadlinkTrap, mBroker);
}
} else {
// No broker; allow the syscalls directly. )-:
switch(sysno) {
case __NR_open:
case __NR_openat:
case __NR_access:
case __NR_faccessat:
@@ -530,34 +601,26 @@ public:
switch (sysno) {
#ifdef DESKTOP
case __NR_getppid:
return Trap(GetPPidTrap, nullptr);
// Filesystem syscalls that need more work to determine who's
// using them, if they need to be, and what we intend to about it.
- case __NR_mkdir:
- case __NR_rmdir:
case __NR_getcwd:
CASES_FOR_statfs:
CASES_FOR_fstatfs:
- case __NR_chmod:
- case __NR_rename:
- case __NR_symlink:
case __NR_quotactl:
- case __NR_link:
- case __NR_unlink:
CASES_FOR_fchown:
case __NR_fchmod:
case __NR_flock:
#endif
return Allow();
- case __NR_readlink:
case __NR_readlinkat:
#ifdef DESKTOP
// Bug 1290896
return Allow();
#else
// Workaround for bug 964455:
return Error(EINVAL);
#endif
--- a/security/sandbox/linux/broker/SandboxBroker.cpp
+++ b/security/sandbox/linux/broker/SandboxBroker.cpp
@@ -2,16 +2,17 @@
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "SandboxBroker.h"
#include "SandboxInfo.h"
#include "SandboxLogging.h"
+#include "SandboxBrokerUtils.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
@@ -26,16 +27,17 @@
#endif
#include "mozilla/Assertions.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Move.h"
#include "mozilla/NullPtr.h"
#include "mozilla/Sprintf.h"
#include "mozilla/ipc/FileDescriptor.h"
+#include "sandbox/linux/system_headers/linux_syscalls.h"
namespace mozilla {
// This constructor signals failure by setting mFileDesc and aClientFd to -1.
SandboxBroker::SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid,
int& aClientFd)
: mChildPid(aChildPid), mPolicy(Move(aPolicy))
{
@@ -278,16 +280,35 @@ SandboxBroker::Policy::Lookup(const nsAC
}
// Strip away the RECURSIVE flag as it doesn't
// necessarily apply to aPath.
return allPerms & ~RECURSIVE;
}
static bool
+AllowOperation(int aReqFlags, int aPerms)
+{
+ int needed = 0;
+ if (aReqFlags & R_OK) {
+ needed |= SandboxBroker::MAY_READ;
+ }
+ if (aReqFlags & W_OK) {
+ needed |= SandboxBroker::MAY_WRITE;
+ }
+ // We don't really allow executing anything,
+ // so in true unix tradition we hijack this
+ // for directories.
+ if (aReqFlags & X_OK) {
+ needed |= SandboxBroker::MAY_CREATE;
+ }
+ return (aPerms & needed) == needed;
+}
+
+static bool
AllowAccess(int aReqFlags, int aPerms)
{
if (aReqFlags & ~(R_OK|W_OK|F_OK)) {
return false;
}
int needed = 0;
if (aReqFlags & R_OK) {
needed |= SandboxBroker::MAY_READ;
@@ -338,22 +359,51 @@ AllowOpen(int aReqFlags, int aPerms)
}
if (aReqFlags & O_CREAT) {
needed |= SandboxBroker::MAY_CREATE;
}
return (aPerms & needed) == needed;
}
static int
-DoStat(const char* aPath, struct stat* aStat, int aFlags)
+DoStat(const char* aPath, void* aBuff, int aFlags)
+{
+ if (aFlags & O_NOFOLLOW) {
+ return lstatsyscall(aPath, (statstruct*)aBuff);
+ }
+ return statsyscall(aPath, (statstruct*)aBuff);
+}
+
+static int
+DoLink(const char* aPath, const char* aPath2,
+ SandboxBrokerCommon::Operation aOper)
{
- if (aFlags & O_NOFOLLOW) {
- return lstat(aPath, aStat);
+ if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_LINK) {
+ return link(aPath, aPath2);
+ } else if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_SYMLINK) {
+ return symlink(aPath, aPath2);
}
- return stat(aPath, aStat);
+ MOZ_CRASH("SandboxBroker: Unknown link operation");
+}
+
+size_t
+SandboxBroker::ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen)
+{
+ if (strstr(aPath, "..") != NULL) {
+ char* result = realpath(aPath, NULL);
+ if (result != NULL) {
+ strncpy(aPath, result, aBufSize);
+ aPath[aBufSize - 1] = '\0';
+ free(result);
+ // Size changed, but guaranteed to be 0 terminated
+ aPathLen = strlen(aPath);
+ }
+ // ValidatePath will handle failure to translate
+ }
+ return aPathLen;
}
void
SandboxBroker::ThreadMain(void)
{
char threadName[16];
SprintfLiteral(threadName, "FS Broker %d", mChildPid);
PlatformThread::SetName(threadName);
@@ -374,27 +424,37 @@ SandboxBroker::ThreadMain(void)
if (syscall(nr_setregid, getgid(), AID_APP + mChildPid) != 0 ||
syscall(nr_setreuid, getuid(), AID_APP + mChildPid) != 0) {
MOZ_CRASH("SandboxBroker: failed to drop privileges");
}
#endif
while (true) {
struct iovec ios[2];
+ // We will receive the path strings in 1 buffer and split them back up.
+ char recvBuf[2 * (kMaxPathLen + 1)];
char pathBuf[kMaxPathLen + 1];
+ char pathBuf2[kMaxPathLen + 1];
size_t pathLen;
- struct stat statBuf;
+ size_t pathLen2;
+ char respBuf[kMaxPathLen + 1]; // Also serves as struct stat
Request req;
Response resp;
int respfd;
+ // Make sure stat responses fit in the response buffer
+ MOZ_ASSERT((kMaxPathLen + 1) > sizeof(struct stat));
+
+ // This makes our string handling below a bit less error prone.
+ memset(recvBuf, 0, sizeof(recvBuf));
+
ios[0].iov_base = &req;
ios[0].iov_len = sizeof(req);
- ios[1].iov_base = pathBuf;
- ios[1].iov_len = kMaxPathLen;
+ ios[1].iov_base = recvBuf;
+ ios[1].iov_len = sizeof(recvBuf);
const ssize_t recvd = RecvWithFd(mFileDesc, ios, 2, &respfd);
if (recvd == 0) {
if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
SANDBOX_LOG_ERROR("EOF from pid %d", mChildPid);
}
break;
}
@@ -416,117 +476,256 @@ SandboxBroker::ThreadMain(void)
if (respfd == -1) {
SANDBOX_LOG_ERROR("no response fd from pid %d", mChildPid);
shutdown(mFileDesc, SHUT_RD);
break;
}
// Initialize the response with the default failure.
memset(&resp, 0, sizeof(resp));
- memset(&statBuf, 0, sizeof(statBuf));
- resp.mError = EACCES;
+ memset(&respBuf, 0, sizeof(respBuf));
+ resp.mError = -EACCES;
ios[0].iov_base = &resp;
ios[0].iov_len = sizeof(resp);
ios[1].iov_base = nullptr;
ios[1].iov_len = 0;
int openedFd = -1;
- // Look up the pathname.
- pathLen = recvd - sizeof(req);
- // It shouldn't be possible for recvmsg to violate this assertion,
- // but one more predictable branch shouldn't have much perf impact:
- MOZ_RELEASE_ASSERT(pathLen <= kMaxPathLen);
- pathBuf[pathLen] = '\0';
- int perms = 0;
- if (!memchr(pathBuf, '\0', pathLen)) {
+ // Clear permissions
+ int perms;
+
+ // Find end of first string, make sure the buffer is still
+ // 0 terminated.
+ size_t recvBufLen = static_cast<size_t>(recvd) - sizeof(req);
+ if (recvBufLen > 0 && recvBuf[recvBufLen - 1] != 0) {
+ SANDBOX_LOG_ERROR("corrupted path buffer from pid %d", mChildPid);
+ shutdown(mFileDesc, SHUT_RD);
+ break;
+ }
+
+ // First path should fit in maximum path length buffer.
+ size_t first_len = strlen(recvBuf);
+ if (first_len <= kMaxPathLen) {
+ strcpy(pathBuf, recvBuf);
+ // Skip right over the terminating 0, and try to copy in the
+ // second path, if any. If there's no path, this will hit a
+ // 0 immediately (we nulled the buffer before receiving).
+ // We do not assume the second path is 0-terminated, this is
+ // enforced below.
+ strncpy(pathBuf2, recvBuf + first_len + 1, kMaxPathLen + 1);
+
+ // First string is guaranteed to be 0-terminated.
+ pathLen = first_len;
+
+ // Look up the first pathname but first translate relative paths.
+ pathLen = ConvertToRealPath(pathBuf, sizeof(pathBuf), pathLen);
perms = mPolicy->Lookup(nsDependentCString(pathBuf, pathLen));
+
+ // Same for the second path.
+ pathLen2 = strnlen(pathBuf2, kMaxPathLen);
+ if (pathLen2 > 0) {
+ // Force 0 termination.
+ pathBuf[pathLen2] = '\0';
+ pathLen2 = ConvertToRealPath(pathBuf2, sizeof(pathBuf2), pathLen2);
+ int perms2 = mPolicy->Lookup(nsDependentCString(pathBuf2, pathLen2));
+
+ // Take the intersection of the permissions for both paths.
+ perms &= perms2;
+ }
+ } else {
+ // Failed to receive intelligible paths.
+ perms = 0;
}
// And now perform the operation if allowed.
if (perms & CRASH_INSTEAD) {
// This is somewhat nonmodular, but it works.
- resp.mError = ENOSYS;
+ resp.mError = -ENOSYS;
} else if (permissive || perms & MAY_ACCESS) {
// If the operation was only allowed because of permissive mode, log it.
if (permissive && !(perms & MAY_ACCESS)) {
- AuditDenial(req.mOp, req.mFlags, pathBuf);
+ AuditPermissive(req.mOp, req.mFlags, perms, pathBuf);
}
switch(req.mOp) {
case SANDBOX_FILE_OPEN:
if (permissive || AllowOpen(req.mFlags, perms)) {
// Permissions for O_CREAT hardwired to 0600; if that's
// ever a problem we can change the protocol (but really we
// should be trying to remove uses of MAY_CREATE, not add
// new ones).
openedFd = open(pathBuf, req.mFlags | kRequiredOpenFlags, 0600);
if (openedFd >= 0) {
resp.mError = 0;
} else {
- resp.mError = errno;
+ resp.mError = -errno;
}
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
}
break;
case SANDBOX_FILE_ACCESS:
if (permissive || AllowAccess(req.mFlags, perms)) {
// This can't use access() itself because that uses the ruid
// and not the euid. In theory faccessat() with AT_EACCESS
// would work, but Linux doesn't actually implement the
// flags != 0 case; glibc has a hack which doesn't even work
// in this case so it'll ignore the flag, and Bionic just
// passes through the syscall and always ignores the flags.
//
// Instead, because we've already checked the requested
// r/w/x bits against the policy, just return success if the
// file exists and hope that's close enough.
- if (stat(pathBuf, &statBuf) == 0) {
+ if (stat(pathBuf, (struct stat*)&respBuf) == 0) {
resp.mError = 0;
} else {
- resp.mError = errno;
+ resp.mError = -errno;
}
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
}
break;
case SANDBOX_FILE_STAT:
- if (DoStat(pathBuf, &statBuf, req.mFlags) == 0) {
+ if (DoStat(pathBuf, (struct stat*)&respBuf, req.mFlags) == 0) {
resp.mError = 0;
- ios[1].iov_base = &statBuf;
- ios[1].iov_len = sizeof(statBuf);
+ ios[1].iov_base = &respBuf;
+ ios[1].iov_len = req.mBufSize;
+ } else {
+ resp.mError = -errno;
+ }
+ break;
+
+ case SANDBOX_FILE_CHMOD:
+ if (permissive || AllowOperation(W_OK, perms)) {
+ if (chmod(pathBuf, req.mFlags) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_LINK:
+ case SANDBOX_FILE_SYMLINK:
+ if (permissive || AllowOperation(W_OK, perms)) {
+ if (DoLink(pathBuf, pathBuf2, req.mOp) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_RENAME:
+ if (permissive || AllowOperation(W_OK, perms)) {
+ if (rename(pathBuf, pathBuf2) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
} else {
- resp.mError = errno;
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_MKDIR:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (mkdir(pathBuf, req.mFlags) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_UNLINK:
+ if (permissive || AllowOperation(W_OK, perms)) {
+ if (unlink(pathBuf) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_RMDIR:
+ if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+ if (rmdir(pathBuf) == 0) {
+ resp.mError = 0;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+ }
+ break;
+
+ case SANDBOX_FILE_READLINK:
+ if (permissive || AllowOperation(R_OK, perms)) {
+ ssize_t respSize = readlink(pathBuf, (char*)&respBuf, sizeof(respBuf));
+ if (respSize >= 0) {
+ resp.mError = respSize;
+ ios[1].iov_base = &respBuf;
+ ios[1].iov_len = respSize;
+ } else {
+ resp.mError = -errno;
+ }
+ } else {
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
}
break;
}
} else {
MOZ_ASSERT(perms == 0);
+ AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
}
const size_t numIO = ios[1].iov_len > 0 ? 2 : 1;
DebugOnly<const ssize_t> sent = SendWithFd(respfd, ios, numIO, openedFd);
close(respfd);
MOZ_ASSERT(sent < 0 ||
static_cast<size_t>(sent) == ios[0].iov_len + ios[1].iov_len);
if (openedFd >= 0) {
close(openedFd);
}
}
}
void
-SandboxBroker::AuditDenial(int aOp, int aFlags, const char* aPath)
+SandboxBroker::AuditPermissive(int aOp, int aFlags, int aPerms, const char* aPath)
{
MOZ_RELEASE_ASSERT(SandboxInfo::Get().Test(SandboxInfo::kPermissive));
struct stat statBuf;
if (lstat(aPath, &statBuf) == 0) {
// Path exists, set errno to 0 to indicate "success".
errno = 0;
}
- SANDBOX_LOG_ERROR("SandboxBroker: denied op=%d rflags=%o path=%s for pid=%d" \
- " permissive=1 error=\"%s\"", aOp, aFlags, aPath, mChildPid,
- strerror(errno));
+ SANDBOX_LOG_ERROR("SandboxBroker: would have denied op=%d rflags=%o perms=%d path=%s for pid=%d" \
+ " permissive=1 error=\"%s\"", aOp, aFlags, aPerms,
+ aPath, mChildPid, strerror(errno));
}
+void
+SandboxBroker::AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath)
+{
+#ifdef DEBUG
+ SANDBOX_LOG_ERROR("SandboxBroker: denied op=%d rflags=%o perms=%d path=%s for pid=%d" \
+ " error=\"%s\"", aOp, aFlags, aPerms, aPath, mChildPid,
+ strerror(errno));
+#endif
+}
+
+
} // namespace mozilla
--- a/security/sandbox/linux/broker/SandboxBroker.h
+++ b/security/sandbox/linux/broker/SandboxBroker.h
@@ -112,17 +112,20 @@ class SandboxBroker final
PlatformThreadHandle mThread;
int mFileDesc;
const int mChildPid;
const UniquePtr<const Policy> mPolicy;
SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid,
int& aClientFd);
void ThreadMain(void) override;
- void AuditDenial(int aOp, int aFlags, const char* aPath);
+ void AuditPermissive(int aOp, int aFlags, int aPerms, const char* aPath);
+ void AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath);
+ // Remap relative paths to absolute paths.
+ size_t ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen);
// Holding a UniquePtr should disallow copying, but to make that explicit:
SandboxBroker(const SandboxBroker&) = delete;
void operator=(const SandboxBroker&) = delete;
};
} // namespace mozilla
--- a/security/sandbox/linux/broker/SandboxBrokerCommon.h
+++ b/security/sandbox/linux/broker/SandboxBrokerCommon.h
@@ -1,16 +1,16 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-#ifndef mozilla_SandboxBrokerTypes_h
-#define mozilla_SandboxBrokerTypes_h
+#ifndef mozilla_SandboxBrokerCommon_h
+#define mozilla_SandboxBrokerCommon_h
#include <sys/types.h>
struct iovec;
// This file defines the protocol between the filesystem broker,
// described in SandboxBroker.h, and its client, described in
// ../SandboxBrokerClient.h; and it defines some utility functions
@@ -24,28 +24,39 @@ struct iovec;
namespace mozilla {
class SandboxBrokerCommon {
public:
enum Operation {
SANDBOX_FILE_OPEN,
SANDBOX_FILE_ACCESS,
SANDBOX_FILE_STAT,
+ SANDBOX_FILE_CHMOD,
+ SANDBOX_FILE_LINK,
+ SANDBOX_FILE_SYMLINK,
+ SANDBOX_FILE_MKDIR,
+ SANDBOX_FILE_RENAME,
+ SANDBOX_FILE_RMDIR,
+ SANDBOX_FILE_UNLINK,
+ SANDBOX_FILE_READLINK,
};
struct Request {
Operation mOp;
// For open, flags; for access, "mode"; for stat, O_NOFOLLOW for lstat.
int mFlags;
+ // Size of return value buffer, if any
+ size_t mBufSize;
// The rest of the packet is the pathname.
// SCM_RIGHTS for response socket attached.
};
struct Response {
- int mError; // errno, or 0 for no error
+ // Syscall result, -errno if failure, or 0 for no error
+ int mError;
// Followed by struct stat for stat/lstat.
// SCM_RIGHTS attached for successful open.
};
// This doesn't need to be the system's maximum path length, just
// the largest path that would be allowed by any policy. (It's used
// to size a stack-allocated buffer.)
static const size_t kMaxPathLen = 4096;
@@ -53,9 +64,9 @@ public:
static ssize_t RecvWithFd(int aFd, const iovec* aIO, size_t aNumIO,
int* aPassedFdPtr);
static ssize_t SendWithFd(int aFd, const iovec* aIO, size_t aNumIO,
int aPassedFd);
};
} // namespace mozilla
-#endif // mozilla_SandboxBrokerTypes_h
+#endif // mozilla_SandboxBrokerCommon_h
--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
@@ -8,16 +8,17 @@
#include "SandboxInfo.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "nsPrintfCString.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"
+#include "SpecialSystemDirectory.h"
#ifdef ANDROID
#include "cutils/properties.h"
#endif
namespace mozilla {
/* static */ bool
@@ -35,22 +36,25 @@ SandboxBrokerPolicyFactory::IsSystemSupp
// automatically regardless of the device.
if (SandboxInfo::Get().Test(SandboxInfo::kPermissive)) {
return true;
}
#endif
return false;
}
-#if defined(MOZ_CONTENT_SANDBOX) && defined(MOZ_WIDGET_GONK)
+#if defined(MOZ_CONTENT_SANDBOX)
namespace {
static const int rdonly = SandboxBroker::MAY_READ;
static const int wronly = SandboxBroker::MAY_WRITE;
static const int rdwr = rdonly | wronly;
+static const int rdwrcr = rdwr | SandboxBroker::MAY_CREATE;
+#if defined(MOZ_WIDGET_GONK)
static const int wrlog = wronly | SandboxBroker::MAY_CREATE;
+#endif
}
#endif
SandboxBrokerPolicyFactory::SandboxBrokerPolicyFactory()
{
// Policy entries that are the same in every process go here, and
// are cached over the lifetime of the factory.
#if defined(MOZ_CONTENT_SANDBOX) && defined(MOZ_WIDGET_GONK)
@@ -106,50 +110,78 @@ SandboxBrokerPolicyFactory::SandboxBroke
// Bug 1198401: timezones. Yes, we need both of these; see bug.
policy->AddTree(rdonly, "/system/usr/share/zoneinfo");
policy->AddTree(rdonly, "/system//usr/share/zoneinfo");
policy->AddPath(rdonly, "/data/local/tmp/profiler.options",
SandboxBroker::Policy::AddAlways); // bug 1029337
mCommonContentPolicy.reset(policy);
+#elif defined(MOZ_CONTENT_SANDBOX)
+ SandboxBroker::Policy* policy = new SandboxBroker::Policy;
+ policy->AddDir(rdonly, "/");
+ policy->AddDir(rdwrcr, "/dev/shm");
+ // Add write permissions on the temporary directory. This can come
+ // from various environment variables (TMPDIR,TMP,TEMP,...) so
+ // make sure to use the full logic.
+ nsCOMPtr<nsIFile> tmpDir;
+ nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory,
+ getter_AddRefs(tmpDir));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString tmpPath;
+ rv = tmpDir->GetNativePath(tmpPath);
+ if (NS_SUCCEEDED(rv)) {
+ policy->AddDir(rdwrcr, tmpPath.get());
+ }
+ }
+ // If the above fails at any point, fall back to a very good guess.
+ if (NS_FAILED(rv)) {
+ policy->AddDir(rdwrcr, "/tmp");
+ }
+ mCommonContentPolicy.reset(policy);
#endif
}
#ifdef MOZ_CONTENT_SANDBOX
UniquePtr<SandboxBroker::Policy>
SandboxBrokerPolicyFactory::GetContentPolicy(int aPid)
{
- // Allow overriding "unsupported"ness with a pref, for testing.
- if (!IsSystemSupported() &&
- Preferences::GetInt("security.sandbox.content.level") <= 1) {
+ // Policy entries that vary per-process (currently the only reason
+ // that can happen is because they contain the pid) are added here.
+
+ MOZ_ASSERT(NS_IsMainThread());
+ // File broker usage is controlled through a pref.
+ if (Preferences::GetInt("security.sandbox.content.level") <= 1) {
return nullptr;
}
- // Policy entries that vary per-process (currently the only reason
- // that can happen is because they contain the pid) are added here.
+ MOZ_ASSERT(mCommonContentPolicy);
#if defined(MOZ_WIDGET_GONK)
- MOZ_ASSERT(NS_IsMainThread());
- MOZ_ASSERT(mCommonContentPolicy);
+ // Allow overriding "unsupported"ness with a pref, for testing.
+ if (!IsSystemSupported()) {
+ return nullptr;
+ }
UniquePtr<SandboxBroker::Policy>
policy(new SandboxBroker::Policy(*mCommonContentPolicy));
// Bug 1029337: where the profiler writes the data.
nsPrintfCString profilerLogPath("/data/local/tmp/profile_%d_%d.txt",
GeckoProcessType_Content, aPid);
policy->AddPath(wrlog, profilerLogPath.get());
// Bug 1198550: the profiler's replacement for dl_iterate_phdr
policy->AddPath(rdonly, nsPrintfCString("/proc/%d/maps", aPid).get());
// Bug 1198552: memory reporting.
policy->AddPath(rdonly, nsPrintfCString("/proc/%d/statm", aPid).get());
policy->AddPath(rdonly, nsPrintfCString("/proc/%d/smaps", aPid).get());
return policy;
-#else // MOZ_WIDGET_GONK
- // Not implemented for desktop yet.
- return nullptr;
+#else
+ UniquePtr<SandboxBroker::Policy>
+ policy(new SandboxBroker::Policy(*mCommonContentPolicy));
+ // Return the common policy.
+ return policy;
#endif
}
#endif // MOZ_CONTENT_SANDBOX
} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerUtils.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_SandboxBrokerUtils_h
+#define mozilla_SandboxBrokerUtils_h
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "sandbox/linux/system_headers/linux_syscalls.h"
+
+// On 32-bit Linux, stat calls are translated by libc into stat64
+// calls. We'll intercept those and handle them in the stat functions
+// but must be sure to use the right structure layout.
+
+#if defined(__NR_stat64)
+typedef struct stat64 statstruct;
+#define statsyscall stat64
+#define lstatsyscall lstat64
+#elif defined(__NR_stat)
+typedef struct stat statstruct;
+#define statsyscall stat
+#define lstatsyscall lstat
+#else
+#error Missing stat syscall include.
+#endif
+
+#endif // mozilla_SandboxBrokerUtils_h
--- a/security/sandbox/linux/gtest/TestBroker.cpp
+++ b/security/sandbox/linux/gtest/TestBroker.cpp
@@ -2,16 +2,17 @@
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "gtest/gtest.h"
#include "broker/SandboxBroker.h"
+#include "broker/SandboxBrokerUtils.h"
#include "SandboxBrokerClient.h"
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdlib.h>
#include <sched.h>
#include <semaphore.h>
@@ -26,17 +27,17 @@
#include "mozilla/UniquePtr.h"
#include "mozilla/ipc/FileDescriptor.h"
namespace mozilla {
static const int MAY_ACCESS = SandboxBroker::MAY_ACCESS;
static const int MAY_READ = SandboxBroker::MAY_READ;
static const int MAY_WRITE = SandboxBroker::MAY_WRITE;
-//static const int MAY_CREATE = SandboxBroker::MAY_CREATE;
+static const int MAY_CREATE = SandboxBroker::MAY_CREATE;
static const auto AddAlways = SandboxBroker::Policy::AddAlways;
class SandboxBrokerTest : public ::testing::Test
{
UniquePtr<SandboxBroker> mServer;
UniquePtr<SandboxBrokerClient> mClient;
UniquePtr<const SandboxBroker::Policy> GetPolicy() const;
@@ -49,22 +50,46 @@ class SandboxBrokerTest : public ::testi
protected:
int Open(const char* aPath, int aFlags) {
return mClient->Open(aPath, aFlags);
}
int Access(const char* aPath, int aMode) {
return mClient->Access(aPath, aMode);
}
- int Stat(const char* aPath, struct stat* aStat) {
+ int Stat(const char* aPath, statstruct* aStat) {
return mClient->Stat(aPath, aStat);
}
- int LStat(const char* aPath, struct stat* aStat) {
+ int LStat(const char* aPath, statstruct* aStat) {
return mClient->LStat(aPath, aStat);
}
+ int Chmod(const char* aPath, int aMode) {
+ return mClient->Chmod(aPath, aMode);
+ }
+ int Link(const char* aPath, const char* bPath) {
+ return mClient->Link(aPath, bPath);
+ }
+ int Mkdir(const char* aPath, int aMode) {
+ return mClient->Mkdir(aPath, aMode);
+ }
+ int Symlink(const char* aPath, const char* bPath) {
+ return mClient->Symlink(aPath, bPath);
+ }
+ int Rename(const char* aPath, const char* bPath) {
+ return mClient->Rename(aPath, bPath);
+ }
+ int Rmdir(const char* aPath) {
+ return mClient->Rmdir(aPath);
+ }
+ int Unlink(const char* aPath) {
+ return mClient->Unlink(aPath);
+ }
+ ssize_t Readlink(const char* aPath, char* aBuff, size_t aSize) {
+ return mClient->Readlink(aPath, aBuff, aSize);
+ }
virtual void SetUp() {
ipc::FileDescriptor fd;
mServer = SandboxBroker::Create(GetPolicy(), getpid(), fd);
ASSERT_NE(mServer, nullptr);
ASSERT_TRUE(fd.IsValid());
auto rawFD = fd.ClonePlatformHandle();
@@ -100,16 +125,19 @@ UniquePtr<const SandboxBroker::Policy>
SandboxBrokerTest::GetPolicy() const
{
UniquePtr<SandboxBroker::Policy> policy(new SandboxBroker::Policy());
policy->AddPath(MAY_READ | MAY_WRITE, "/dev/null", AddAlways);
policy->AddPath(MAY_READ, "/dev/zero", AddAlways);
policy->AddPath(MAY_READ, "/var/empty/qwertyuiop", AddAlways);
policy->AddPath(MAY_ACCESS, "/proc/self", AddAlways); // Warning: Linux-specific.
+ policy->AddPath(MAY_READ | MAY_WRITE, "/tmp", AddAlways);
+ policy->AddPath(MAY_READ | MAY_WRITE | MAY_CREATE, "/tmp/blublu", AddAlways);
+ policy->AddPath(MAY_READ | MAY_WRITE | MAY_CREATE, "/tmp/blublublu", AddAlways);
return Move(policy);
}
TEST_F(SandboxBrokerTest, OpenForRead)
{
int fd;
@@ -177,44 +205,201 @@ TEST_F(SandboxBrokerTest, Access)
EXPECT_EQ(0, Access("/proc/self", F_OK));
EXPECT_EQ(-EACCES, Access("/proc/self", R_OK));
EXPECT_EQ(-EACCES, Access("/proc/self/stat", F_OK));
}
TEST_F(SandboxBrokerTest, Stat)
{
- struct stat brokeredStat, realStat;
- ASSERT_EQ(0, stat("/dev/null", &realStat)) << "Shouldn't ever fail!";
+ statstruct realStat, brokeredStat;
+ ASSERT_EQ(0, statsyscall("/dev/null", &realStat)) << "Shouldn't ever fail!";
EXPECT_EQ(0, Stat("/dev/null", &brokeredStat));
EXPECT_EQ(realStat.st_ino, brokeredStat.st_ino);
EXPECT_EQ(realStat.st_rdev, brokeredStat.st_rdev);
EXPECT_EQ(-ENOENT, Stat("/var/empty/qwertyuiop", &brokeredStat));
EXPECT_EQ(-EACCES, Stat("/dev", &brokeredStat));
EXPECT_EQ(0, Stat("/proc/self", &brokeredStat));
EXPECT_TRUE(S_ISDIR(brokeredStat.st_mode));
}
TEST_F(SandboxBrokerTest, LStat)
{
- struct stat brokeredStat, realStat;
- ASSERT_EQ(0, lstat("/dev/null", &realStat));
+ statstruct realStat, brokeredStat;
+ ASSERT_EQ(0, lstatsyscall("/dev/null", &realStat));
EXPECT_EQ(0, LStat("/dev/null", &brokeredStat));
EXPECT_EQ(realStat.st_ino, brokeredStat.st_ino);
EXPECT_EQ(realStat.st_rdev, brokeredStat.st_rdev);
EXPECT_EQ(-ENOENT, LStat("/var/empty/qwertyuiop", &brokeredStat));
EXPECT_EQ(-EACCES, LStat("/dev", &brokeredStat));
EXPECT_EQ(0, LStat("/proc/self", &brokeredStat));
EXPECT_TRUE(S_ISLNK(brokeredStat.st_mode));
}
+static void PrePostTestCleanup(void)
+{
+ unlink("/tmp/blublu");
+ rmdir("/tmp/blublu");
+ unlink("/tmp/nope");
+ rmdir("/tmp/nope");
+ unlink("/tmp/blublublu");
+ rmdir("/tmp/blublublu");
+}
+
+TEST_F(SandboxBrokerTest, Chmod)
+{
+ PrePostTestCleanup();
+
+ int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+ ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+ close(fd);
+ // Set read only. SandboxBroker enforces 0600 mode flags.
+ ASSERT_EQ(0, Chmod("/tmp/blublu", S_IRUSR));
+ // SandboxBroker doesn't use real access(), it just checks against
+ // the policy. So it can't see the change in permisions here.
+ // This won't work:
+ // EXPECT_EQ(-EACCES, Access("/tmp/blublu", W_OK));
+ statstruct realStat;
+ EXPECT_EQ(0, statsyscall("/tmp/blublu", &realStat));
+ EXPECT_EQ((mode_t)S_IRUSR, realStat.st_mode & 0777);
+
+ ASSERT_EQ(0, Chmod("/tmp/blublu", S_IRUSR | S_IWUSR));
+ EXPECT_EQ(0, statsyscall("/tmp/blublu", &realStat));
+ EXPECT_EQ((mode_t)(S_IRUSR | S_IWUSR), realStat.st_mode & 0777);
+ EXPECT_EQ(0, unlink("/tmp/blublu"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Link)
+{
+ PrePostTestCleanup();
+
+ int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+ ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+ close(fd);
+ ASSERT_EQ(0, Link("/tmp/blublu", "/tmp/blublublu"));
+ EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+ // Not whitelisted target path
+ EXPECT_EQ(-EACCES, Link("/tmp/blublu", "/tmp/nope"));
+ EXPECT_EQ(0, unlink("/tmp/blublublu"));
+ EXPECT_EQ(0, unlink("/tmp/blublu"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Symlink)
+{
+ PrePostTestCleanup();
+
+ int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+ ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+ close(fd);
+ ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu"));
+ EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+ statstruct aStat;
+ ASSERT_EQ(0, lstatsyscall("/tmp/blublublu", &aStat));
+ EXPECT_EQ((mode_t)S_IFLNK, aStat.st_mode & S_IFMT);
+ // Not whitelisted target path
+ EXPECT_EQ(-EACCES, Symlink("/tmp/blublu", "/tmp/nope"));
+ EXPECT_EQ(0, unlink("/tmp/blublublu"));
+ EXPECT_EQ(0, unlink("/tmp/blublu"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Mkdir)
+{
+ PrePostTestCleanup();
+
+ ASSERT_EQ(0, mkdir("/tmp/blublu", 0600))
+ << "Creating dir /tmp/blublu failed.";
+ EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+ // Not whitelisted target path
+ EXPECT_EQ(-EACCES, Mkdir("/tmp/nope", 0600))
+ << "Creating dir without MAY_CREATE succeed.";
+ EXPECT_EQ(0, rmdir("/tmp/blublu"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Rename)
+{
+ PrePostTestCleanup();
+
+ ASSERT_EQ(0, mkdir("/tmp/blublu", 0600))
+ << "Creating dir /tmp/blublu failed.";
+ EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+ ASSERT_EQ(0, Rename("/tmp/blublu", "/tmp/blublublu"));
+ EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+ EXPECT_EQ(-ENOENT , Access("/tmp/blublu", F_OK));
+ // Not whitelisted target path
+ EXPECT_EQ(-EACCES, Rename("/tmp/blublublu", "/tmp/nope"))
+ << "Renaming dir without write access succeed.";
+ EXPECT_EQ(0, rmdir("/tmp/blublublu"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Rmdir)
+{
+ PrePostTestCleanup();
+
+ ASSERT_EQ(0, mkdir("/tmp/blublu", 0600))
+ << "Creating dir /tmp/blublu failed.";
+ EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+ ASSERT_EQ(0, Rmdir("/tmp/blublu"));
+ EXPECT_EQ(-ENOENT, Access("/tmp/blublu", F_OK));
+ // Bypass sandbox to create a non-deletable dir
+ ASSERT_EQ(0, mkdir("/tmp/nope", 0600));
+ EXPECT_EQ(-EACCES, Rmdir("/tmp/nope"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Unlink)
+{
+ PrePostTestCleanup();
+
+ int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+ ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+ close(fd);
+ EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+ EXPECT_EQ(0, Unlink("/tmp/blublu"));
+ EXPECT_EQ(-ENOENT , Access("/tmp/blublu", F_OK));
+ // Bypass sandbox to write a non-deletable file
+ fd = open("/tmp/nope", O_WRONLY | O_CREAT, 0600);
+ ASSERT_GE(fd, 0) << "Opening /tmp/nope for writing failed.";
+ close(fd);
+ EXPECT_EQ(-EACCES, Unlink("/tmp/nope"));
+
+ PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Readlink)
+{
+ PrePostTestCleanup();
+
+ int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+ ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+ close(fd);
+ ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu"));
+ EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+ char linkBuff[256];
+ EXPECT_EQ(11, Readlink("/tmp/blublublu", linkBuff, sizeof(linkBuff)));
+ linkBuff[11] = '\0';
+ EXPECT_EQ(0, strcmp(linkBuff, "/tmp/blublu"));
+
+ PrePostTestCleanup();
+}
+
TEST_F(SandboxBrokerTest, MultiThreadOpen) {
RunOnManyThreads<SandboxBrokerTest,
&SandboxBrokerTest::MultiThreadOpenWorker>();
}
void SandboxBrokerTest::MultiThreadOpenWorker() {
static const int kNumLoops = 10000;
for (int i = 1; i <= kNumLoops; ++i) {
@@ -232,23 +417,23 @@ void SandboxBrokerTest::MultiThreadOpenW
}
TEST_F(SandboxBrokerTest, MultiThreadStat) {
RunOnManyThreads<SandboxBrokerTest,
&SandboxBrokerTest::MultiThreadStatWorker>();
}
void SandboxBrokerTest::MultiThreadStatWorker() {
static const int kNumLoops = 7500;
- struct stat nullStat, zeroStat, selfStat;
+ statstruct nullStat, zeroStat, selfStat;
dev_t realNullDev, realZeroDev;
ino_t realSelfInode;
- ASSERT_EQ(0, stat("/dev/null", &nullStat)) << "Shouldn't ever fail!";
- ASSERT_EQ(0, stat("/dev/zero", &zeroStat)) << "Shouldn't ever fail!";
- ASSERT_EQ(0, lstat("/proc/self", &selfStat)) << "Shouldn't ever fail!";
+ ASSERT_EQ(0, statsyscall("/dev/null", &nullStat)) << "Shouldn't ever fail!";
+ ASSERT_EQ(0, statsyscall("/dev/zero", &zeroStat)) << "Shouldn't ever fail!";
+ ASSERT_EQ(0, lstatsyscall("/proc/self", &selfStat)) << "Shouldn't ever fail!";
ASSERT_TRUE(S_ISLNK(selfStat.st_mode)) << "Shouldn't ever fail!";
realNullDev = nullStat.st_rdev;
realZeroDev = zeroStat.st_rdev;
realSelfInode = selfStat.st_ino;
for (int i = 1; i <= kNumLoops; ++i) {
ASSERT_EQ(0, Stat("/dev/null", &nullStat))
<< "Loop " << i << "/" << kNumLoops;
ASSERT_EQ(0, Stat("/dev/zero", &zeroStat))
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -606,23 +606,23 @@ Collection.prototype = {
// index
get sort() { return this._sort; },
set sort(value) {
this._sort = value;
this._rebuildURL();
},
// Set information about the batch for this request.
- get batch() { return _batch; },
+ get batch() { return this._batch; },
set batch(value) {
this._batch = value;
this._rebuildURL();
},
- get commit() { return _commit; },
+ get commit() { return this._commit; },
set commit(value) {
this._commit = value && true;
this._rebuildURL();
},
set recordHandler(onRecord) {
// Save this because onProgress is called with this as the ChannelListener
let coll = this;
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/head_errorhandler_common.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/engines.js");
+
+// Common code for test_errorhandler_{1,2}.js -- pulled out to make it less
+// monolithic and take less time to execute.
+const EHTestsCommon = {
+
+ service_unavailable(request, response) {
+ let body = "Service Unavailable";
+ response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
+ response.setHeader("Retry-After", "42");
+ response.bodyOutputStream.write(body, body.length);
+ },
+
+ sync_httpd_setup() {
+ let global = new ServerWBO("global", {
+ syncID: Service.syncID,
+ storageVersion: STORAGE_VERSION,
+ engines: {clients: {version: Service.clientsEngine.version,
+ syncID: Service.clientsEngine.syncID},
+ catapult: {version: Service.engineManager.get("catapult").version,
+ syncID: Service.engineManager.get("catapult").syncID}}
+ });
+ let clientsColl = new ServerCollection({}, true);
+
+ // Tracking info/collections.
+ let collectionsHelper = track_collections_helper();
+ let upd = collectionsHelper.with_updated_collection;
+
+ let handler_401 = httpd_handler(401, "Unauthorized");
+ return httpd_setup({
+ // Normal server behaviour.
+ "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
+ "/1.1/johndoe/info/collections": collectionsHelper.handler,
+ "/1.1/johndoe/storage/crypto/keys":
+ upd("crypto", (new ServerWBO("keys")).handler()),
+ "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
+
+ // Credentials are wrong or node reallocated.
+ "/1.1/janedoe/storage/meta/global": handler_401,
+ "/1.1/janedoe/info/collections": handler_401,
+
+ // Maintenance or overloaded (503 + Retry-After) at info/collections.
+ "/maintenance/1.1/broken.info/info/collections": EHTestsCommon.service_unavailable,
+
+ // Maintenance or overloaded (503 + Retry-After) at meta/global.
+ "/maintenance/1.1/broken.meta/storage/meta/global": EHTestsCommon.service_unavailable,
+ "/maintenance/1.1/broken.meta/info/collections": collectionsHelper.handler,
+
+ // Maintenance or overloaded (503 + Retry-After) at crypto/keys.
+ "/maintenance/1.1/broken.keys/storage/meta/global": upd("meta", global.handler()),
+ "/maintenance/1.1/broken.keys/info/collections": collectionsHelper.handler,
+ "/maintenance/1.1/broken.keys/storage/crypto/keys": EHTestsCommon.service_unavailable,
+
+ // Maintenance or overloaded (503 + Retry-After) at wiping collection.
+ "/maintenance/1.1/broken.wipe/info/collections": collectionsHelper.handler,
+ "/maintenance/1.1/broken.wipe/storage/meta/global": upd("meta", global.handler()),
+ "/maintenance/1.1/broken.wipe/storage/crypto/keys":
+ upd("crypto", (new ServerWBO("keys")).handler()),
+ "/maintenance/1.1/broken.wipe/storage": EHTestsCommon.service_unavailable,
+ "/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
+ "/maintenance/1.1/broken.wipe/storage/catapult": EHTestsCommon.service_unavailable
+ });
+ },
+
+ CatapultEngine: (function() {
+ function CatapultEngine() {
+ SyncEngine.call(this, "Catapult", Service);
+ }
+ CatapultEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ exception: null, // tests fill this in
+ _sync: function _sync() {
+ if (this.exception) {
+ throw this.exception;
+ }
+ }
+ };
+
+ return CatapultEngine;
+ }()),
+
+
+ generateCredentialsChangedFailure() {
+ // Make sync fail due to changed credentials. We simply re-encrypt
+ // the keys with a different Sync Key, without changing the local one.
+ let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
+ let keys = Service.collectionKeys.asWBO();
+ keys.encrypt(newSyncKeyBundle);
+ keys.upload(Service.resource(Service.cryptoKeysURL));
+ },
+
+ setUp(server) {
+ return configureIdentity({ username: "johndoe" }).then(
+ () => {
+ Service.serverURL = server.baseURI + "/";
+ Service.clusterURL = server.baseURI + "/";
+ }
+ ).then(
+ () => EHTestsCommon.generateAndUploadKeys()
+ );
+ },
+
+ generateAndUploadKeys() {
+ generateNewKeys(Service.collectionKeys);
+ let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+ serverKeys.encrypt(Service.identity.syncKeyBundle);
+ return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
+ }
+};
deleted file mode 100644
--- a/services/sync/tests/unit/test_errorhandler.js
+++ /dev/null
@@ -1,1956 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/policies.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-Cu.import("resource://gre/modules/FileUtils.jsm");
-
-var fakeServer = new SyncServer();
-fakeServer.start();
-
-do_register_cleanup(function() {
- return new Promise(resolve => {
- fakeServer.stop(resolve);
- });
-});
-
-var fakeServerUrl = "http://localhost:" + fakeServer.port;
-
-const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
-
-const PROLONGED_ERROR_DURATION =
- (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
-
-const NON_PROLONGED_ERROR_DURATION =
- (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
-
-Service.engineManager.clear();
-
-function setLastSync(lastSyncValue) {
- Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
-}
-
-function CatapultEngine() {
- SyncEngine.call(this, "Catapult", Service);
-}
-CatapultEngine.prototype = {
- __proto__: SyncEngine.prototype,
- exception: null, // tests fill this in
- _sync: function _sync() {
- if (this.exception) {
- throw this.exception;
- }
- }
-};
-
-var engineManager = Service.engineManager;
-engineManager.register(CatapultEngine);
-
-// This relies on Service/ErrorHandler being a singleton. Fixing this will take
-// a lot of work.
-var errorHandler = Service.errorHandler;
-
-function run_test() {
- initTestLogging("Trace");
-
- Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
-
- ensureLegacyIdentityManager();
-
- run_next_test();
-}
-
-function generateCredentialsChangedFailure() {
- // Make sync fail due to changed credentials. We simply re-encrypt
- // the keys with a different Sync Key, without changing the local one.
- let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
- let keys = Service.collectionKeys.asWBO();
- keys.encrypt(newSyncKeyBundle);
- keys.upload(Service.resource(Service.cryptoKeysURL));
-}
-
-function service_unavailable(request, response) {
- let body = "Service Unavailable";
- response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
- response.setHeader("Retry-After", "42");
- response.bodyOutputStream.write(body, body.length);
-}
-
-function sync_httpd_setup() {
- let global = new ServerWBO("global", {
- syncID: Service.syncID,
- storageVersion: STORAGE_VERSION,
- engines: {clients: {version: Service.clientsEngine.version,
- syncID: Service.clientsEngine.syncID},
- catapult: {version: engineManager.get("catapult").version,
- syncID: engineManager.get("catapult").syncID}}
- });
- let clientsColl = new ServerCollection({}, true);
-
- // Tracking info/collections.
- let collectionsHelper = track_collections_helper();
- let upd = collectionsHelper.with_updated_collection;
-
- let handler_401 = httpd_handler(401, "Unauthorized");
- return httpd_setup({
- // Normal server behaviour.
- "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
- "/1.1/johndoe/info/collections": collectionsHelper.handler,
- "/1.1/johndoe/storage/crypto/keys":
- upd("crypto", (new ServerWBO("keys")).handler()),
- "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
-
- // Credentials are wrong or node reallocated.
- "/1.1/janedoe/storage/meta/global": handler_401,
- "/1.1/janedoe/info/collections": handler_401,
-
- // Maintenance or overloaded (503 + Retry-After) at info/collections.
- "/maintenance/1.1/broken.info/info/collections": service_unavailable,
-
- // Maintenance or overloaded (503 + Retry-After) at meta/global.
- "/maintenance/1.1/broken.meta/storage/meta/global": service_unavailable,
- "/maintenance/1.1/broken.meta/info/collections": collectionsHelper.handler,
-
- // Maintenance or overloaded (503 + Retry-After) at crypto/keys.
- "/maintenance/1.1/broken.keys/storage/meta/global": upd("meta", global.handler()),
- "/maintenance/1.1/broken.keys/info/collections": collectionsHelper.handler,
- "/maintenance/1.1/broken.keys/storage/crypto/keys": service_unavailable,
-
- // Maintenance or overloaded (503 + Retry-After) at wiping collection.
- "/maintenance/1.1/broken.wipe/info/collections": collectionsHelper.handler,
- "/maintenance/1.1/broken.wipe/storage/meta/global": upd("meta", global.handler()),
- "/maintenance/1.1/broken.wipe/storage/crypto/keys":
- upd("crypto", (new ServerWBO("keys")).handler()),
- "/maintenance/1.1/broken.wipe/storage": service_unavailable,
- "/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
- "/maintenance/1.1/broken.wipe/storage/catapult": service_unavailable
- });
-}
-
-function setUp(server) {
- return configureIdentity({username: "johndoe"}).then(
- () => {
- Service.serverURL = server.baseURI + "/";
- Service.clusterURL = server.baseURI + "/";
- }
- ).then(
- () => generateAndUploadKeys()
- );
-}
-
-function generateAndUploadKeys() {
- generateNewKeys(Service.collectionKeys);
- let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
- serverKeys.encrypt(Service.identity.syncKeyBundle);
- return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
-}
-
-function clean() {
- Service.startOver();
- Status.resetSync();
- Status.resetBackoff();
- errorHandler.didReportProlongedError = false;
-}
-
-add_identity_test(this, function* test_401_logout() {
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // By calling sync, we ensure we're logged in.
- yield sync_and_validate_telem();
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(Service.isLoggedIn);
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:service:sync:error", onSyncError);
- function onSyncError() {
- _("Got weave:service:sync:error in first sync.");
- Svc.Obs.remove("weave:service:sync:error", onSyncError);
-
- // Wait for the automatic next sync.
- function onLoginError() {
- _("Got weave:service:login:error in second sync.");
- Svc.Obs.remove("weave:service:login:error", onLoginError);
-
- let expected = isConfiguredWithLegacyIdentity() ?
- LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR;
-
- do_check_eq(Status.login, expected);
- do_check_false(Service.isLoggedIn);
-
- // Clean up.
- Utils.nextTick(function () {
- Service.startOver();
- server.stop(deferred.resolve);
- });
- }
- Svc.Obs.add("weave:service:login:error", onLoginError);
- }
-
- // Make sync fail due to login rejected.
- yield configureIdentity({username: "janedoe"});
- Service._updateCachedURLs();
-
- _("Starting first sync.");
- let ping = yield sync_and_validate_telem(true);
- deepEqual(ping.failureReason, { name: "httperror", code: 401 });
- _("First sync done.");
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_credentials_changed_logout() {
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // By calling sync, we ensure we're logged in.
- yield sync_and_validate_telem();
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(Service.isLoggedIn);
-
- generateCredentialsChangedFailure();
-
- let ping = yield sync_and_validate_telem(true);
- equal(ping.status.sync, CREDENTIALS_CHANGED);
- deepEqual(ping.failureReason, {
- name: "unexpectederror",
- error: "Error: Aborting sync, remote setup failed"
- });
-
- do_check_eq(Status.sync, CREDENTIALS_CHANGED);
- do_check_false(Service.isLoggedIn);
-
- // Clean up.
- Service.startOver();
- let deferred = Promise.defer();
- server.stop(deferred.resolve);
- yield deferred.promise;
-});
-
-add_identity_test(this, function test_no_lastSync_pref() {
- // Test reported error.
- Status.resetSync();
- errorHandler.dontIgnoreErrors = true;
- Status.sync = CREDENTIALS_CHANGED;
- do_check_true(errorHandler.shouldReportError());
-
- // Test unreported error.
- Status.resetSync();
- errorHandler.dontIgnoreErrors = true;
- Status.login = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
-
-});
-
-add_identity_test(this, function test_shouldReportError() {
- Status.login = MASTER_PASSWORD_LOCKED;
- do_check_false(errorHandler.shouldReportError());
-
- // Give ourselves a clusterURL so that the temporary 401 no-error situation
- // doesn't come into play.
- Service.serverURL = fakeServerUrl;
- Service.clusterURL = fakeServerUrl;
-
- // Test dontIgnoreErrors, non-network, non-prolonged, login error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.login = LOGIN_FAILED_NO_PASSWORD;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.sync = CREDENTIALS_CHANGED;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, non-network, prolonged, login error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.login = LOGIN_FAILED_NO_PASSWORD;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, non-network, prolonged, sync error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.sync = CREDENTIALS_CHANGED;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, network, non-prolonged, login error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.login = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, network, non-prolonged, sync error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.sync = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, network, prolonged, login error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.login = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
-
- // Test dontIgnoreErrors, network, prolonged, sync error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.sync = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
-
- // Test non-network, prolonged, login error reported
- do_check_false(errorHandler.didReportProlongedError);
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = LOGIN_FAILED_NO_PASSWORD;
- do_check_true(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
-
- // Second time with prolonged error and without resetting
- // didReportProlongedError, sync error should not be reported.
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = LOGIN_FAILED_NO_PASSWORD;
- do_check_false(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
-
- // Test non-network, prolonged, sync error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- errorHandler.didReportProlongedError = false;
- Status.sync = CREDENTIALS_CHANGED;
- do_check_true(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
- errorHandler.didReportProlongedError = false;
-
- // Test network, prolonged, login error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
- errorHandler.didReportProlongedError = false;
-
- // Test network, prolonged, sync error reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.sync = LOGIN_FAILED_NETWORK_ERROR;
- do_check_true(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
- errorHandler.didReportProlongedError = false;
-
- // Test non-network, non-prolonged, login error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = LOGIN_FAILED_NO_PASSWORD;
- do_check_true(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test non-network, non-prolonged, sync error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.sync = CREDENTIALS_CHANGED;
- do_check_true(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test network, non-prolonged, login error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = LOGIN_FAILED_NETWORK_ERROR;
- do_check_false(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test network, non-prolonged, sync error reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.sync = LOGIN_FAILED_NETWORK_ERROR;
- do_check_false(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test server maintenance, sync errors are not reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.sync = SERVER_MAINTENANCE;
- do_check_false(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test server maintenance, login errors are not reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = SERVER_MAINTENANCE;
- do_check_false(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test prolonged, server maintenance, sync errors are reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.sync = SERVER_MAINTENANCE;
- do_check_true(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
- errorHandler.didReportProlongedError = false;
-
- // Test prolonged, server maintenance, login errors are reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = false;
- Status.login = SERVER_MAINTENANCE;
- do_check_true(errorHandler.shouldReportError());
- do_check_true(errorHandler.didReportProlongedError);
- errorHandler.didReportProlongedError = false;
-
- // Test dontIgnoreErrors, server maintenance, sync errors are reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.sync = SERVER_MAINTENANCE;
- do_check_true(errorHandler.shouldReportError());
- // dontIgnoreErrors means we don't set didReportProlongedError
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test dontIgnoreErrors, server maintenance, login errors are reported
- Status.resetSync();
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.login = SERVER_MAINTENANCE;
- do_check_true(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test dontIgnoreErrors, prolonged, server maintenance,
- // sync errors are reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.sync = SERVER_MAINTENANCE;
- do_check_true(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-
- // Test dontIgnoreErrors, prolonged, server maintenance,
- // login errors are reported
- Status.resetSync();
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.dontIgnoreErrors = true;
- Status.login = SERVER_MAINTENANCE;
- do_check_true(errorHandler.shouldReportError());
- do_check_false(errorHandler.didReportProlongedError);
-});
-
-add_identity_test(this, function* test_shouldReportError_master_password() {
- _("Test error ignored due to locked master password");
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // Monkey patch Service.verifyLogin to imitate
- // master password being locked.
- Service._verifyLogin = Service.verifyLogin;
- Service.verifyLogin = function () {
- Status.login = MASTER_PASSWORD_LOCKED;
- return false;
- };
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- do_check_false(errorHandler.shouldReportError());
-
- // Clean up.
- Service.verifyLogin = Service._verifyLogin;
- clean();
- let deferred = Promise.defer();
- server.stop(deferred.resolve);
- yield deferred.promise;
-});
-
-// Test that even if we don't have a cluster URL, a login failure due to
-// authentication errors is always reported.
-add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
- // Ensure no clusterURL - any error not specific to login should not be reported.
- Service.serverURL = "";
- Service.clusterURL = "";
-
- // Test explicit "login rejected" state.
- Status.resetSync();
- // If we have a LOGIN_REJECTED state, we always report the error.
- Status.login = LOGIN_FAILED_LOGIN_REJECTED;
- do_check_true(errorHandler.shouldReportError());
- // But any other status with a missing clusterURL is treated as a mid-sync
- // 401 (ie, should be treated as a node reassignment)
- Status.login = LOGIN_SUCCEEDED;
- do_check_false(errorHandler.shouldReportError());
-});
-
-// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
-// an fxaccounts environment?
-add_task(function* test_login_syncAndReportErrors_non_network_error() {
- // Test non-network errors are reported
- // when calling syncAndReportErrors
- let server = sync_httpd_setup();
- yield setUp(server);
- Service.identity.basicPassword = null;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_sync_syncAndReportErrors_non_network_error() {
- // Test non-network errors are reported
- // when calling syncAndReportErrors
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // By calling sync, we ensure we're logged in.
- Service.sync();
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(Service.isLoggedIn);
-
- generateCredentialsChangedFailure();
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, CREDENTIALS_CHANGED);
- // If we clean this tick, telemetry won't get the right error
- server.stop(() => {
- clean();
- deferred.resolve();
- });
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
- equal(ping.status.sync, CREDENTIALS_CHANGED);
- deepEqual(ping.failureReason, {
- name: "unexpectederror",
- error: "Error: Aborting sync, remote setup failed"
- });
- yield deferred.promise;
-});
-
-// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
-// an fxaccounts environment?
-add_task(function* test_login_syncAndReportErrors_prolonged_non_network_error() {
- // Test prolonged, non-network errors are
- // reported when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
- Service.identity.basicPassword = null;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_sync_syncAndReportErrors_prolonged_non_network_error() {
- // Test prolonged, non-network errors are
- // reported when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // By calling sync, we ensure we're logged in.
- Service.sync();
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(Service.isLoggedIn);
-
- generateCredentialsChangedFailure();
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, CREDENTIALS_CHANGED);
- // If we clean this tick, telemetry won't get the right error
- server.stop(() => {
- clean();
- deferred.resolve();
- });
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
- equal(ping.status.sync, CREDENTIALS_CHANGED);
- deepEqual(ping.failureReason, {
- name: "unexpectederror",
- error: "Error: Aborting sync, remote setup failed"
- });
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_login_syncAndReportErrors_network_error() {
- // Test network errors are reported when calling syncAndReportErrors.
- yield configureIdentity({username: "broken.wipe"});
- Service.serverURL = fakeServerUrl;
- Service.clusterURL = fakeServerUrl;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
-
- clean();
- deferred.resolve();
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-
-add_test(function test_sync_syncAndReportErrors_network_error() {
- // Test network errors are reported when calling syncAndReportErrors.
- Services.io.offline = true;
-
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-
- Services.io.offline = false;
- clean();
- run_next_test();
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
-});
-
-add_identity_test(this, function* test_login_syncAndReportErrors_prolonged_network_error() {
- // Test prolonged, network errors are reported
- // when calling syncAndReportErrors.
- yield configureIdentity({username: "johndoe"});
-
- Service.serverURL = fakeServerUrl;
- Service.clusterURL = fakeServerUrl;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
-
- clean();
- deferred.resolve();
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
- // Test prolonged, network errors are reported
- // when calling syncAndReportErrors.
- Services.io.offline = true;
-
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-
- Services.io.offline = false;
- clean();
- run_next_test();
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
-});
-
-add_task(function* test_login_prolonged_non_network_error() {
- // Test prolonged, non-network errors are reported
- let server = sync_httpd_setup();
- yield setUp(server);
- Service.identity.basicPassword = null;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_task(function* test_sync_prolonged_non_network_error() {
- // Test prolonged, non-network errors are reported
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // By calling sync, we ensure we're logged in.
- Service.sync();
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(Service.isLoggedIn);
-
- generateCredentialsChangedFailure();
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
- server.stop(() => {
- clean();
- deferred.resolve();
- });
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
-
- let ping = yield sync_and_validate_telem(true);
- equal(ping.status.sync, PROLONGED_SYNC_FAILURE);
- deepEqual(ping.failureReason, {
- name: "unexpectederror",
- error: "Error: Aborting sync, remote setup failed"
- });
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_login_prolonged_network_error() {
- // Test prolonged, network errors are reported
- yield configureIdentity({username: "johndoe"});
- Service.serverURL = fakeServerUrl;
- Service.clusterURL = fakeServerUrl;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- deferred.resolve();
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_test(function test_sync_prolonged_network_error() {
- // Test prolonged, network errors are reported
- Services.io.offline = true;
-
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- Services.io.offline = false;
- clean();
- run_next_test();
- });
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
-});
-
-add_task(function* test_login_non_network_error() {
- // Test non-network errors are reported
- let server = sync_httpd_setup();
- yield setUp(server);
- Service.identity.basicPassword = null;
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:login:error", onSyncError);
- do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_task(function* test_sync_non_network_error() {
- // Test non-network errors are reported
- let server = sync_httpd_setup();
- yield setUp(server);
-
- // By calling sync, we ensure we're logged in.
- Service.sync();
- do_check_eq(Status.sync, SYNC_SUCCEEDED);
- do_check_true(Service.isLoggedIn);
-
- generateCredentialsChangedFailure();
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- do_check_eq(Status.sync, CREDENTIALS_CHANGED);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_login_network_error() {
- yield configureIdentity({username: "johndoe"});
- Service.serverURL = fakeServerUrl;
- Service.clusterURL = fakeServerUrl;
-
- let deferred = Promise.defer();
- // Test network errors are not reported.
- Svc.Obs.add("weave:ui:clear-error", function onClearError() {
- Svc.Obs.remove("weave:ui:clear-error", onClearError);
-
- do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
- do_check_false(errorHandler.didReportProlongedError);
-
- Services.io.offline = false;
- clean();
- deferred.resolve()
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_test(function test_sync_network_error() {
- // Test network errors are not reported.
- Services.io.offline = true;
-
- Svc.Obs.add("weave:ui:sync:finish", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:sync:finish", onUIUpdate);
- do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
- do_check_false(errorHandler.didReportProlongedError);
-
- Services.io.offline = false;
- clean();
- run_next_test();
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
-});
-
-add_identity_test(this, function* test_sync_server_maintenance_error() {
- // Test server maintenance errors are not reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- const BACKOFF = 42;
- let engine = engineManager.get("catapult");
- engine.enabled = true;
- engine.exception = {status: 503,
- headers: {"retry-after": BACKOFF}};
-
- function onSyncError() {
- do_throw("Shouldn't get here!");
- }
- Svc.Obs.add("weave:ui:sync:error", onSyncError);
-
- do_check_eq(Status.service, STATUS_OK);
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:finish", function onSyncFinish() {
- Svc.Obs.remove("weave:ui:sync:finish", onSyncFinish);
-
- do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
- do_check_eq(Status.sync, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- Svc.Obs.remove("weave:ui:sync:error", onSyncError);
- server.stop(() => {
- clean();
- deferred.resolve();
- })
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- let ping = yield sync_and_validate_telem(true);
- equal(ping.status.sync, SERVER_MAINTENANCE);
- deepEqual(ping.engines.find(e => e.failureReason).failureReason, { name: "httperror", code: 503 })
-
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_server_maintenance_error() {
- // Test info/collections server maintenance errors are not reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- Service.username = "broken.info";
- yield configureIdentity({username: "broken.info"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- function onUIUpdate() {
- do_throw("Shouldn't experience UI update!");
- }
- Svc.Obs.add("weave:ui:login:error", onUIUpdate);
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
- Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
-
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_server_maintenance_error() {
- // Test meta/global server maintenance errors are not reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.meta"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- function onUIUpdate() {
- do_throw("Shouldn't get here!");
- }
- Svc.Obs.add("weave:ui:login:error", onUIUpdate);
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
- Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
-
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_crypto_keys_login_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are not reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- // Force re-download of keys
- Service.collectionKeys.clear();
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- function onUIUpdate() {
- do_throw("Shouldn't get here!");
- }
- Svc.Obs.add("weave:ui:login:error", onUIUpdate);
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
- Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
-
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- clean();
- server.stop(deferred.resolve);
- });
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_task(function* test_sync_prolonged_server_maintenance_error() {
- // Test prolonged server maintenance errors are reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- const BACKOFF = 42;
- let engine = engineManager.get("catapult");
- engine.enabled = true;
- engine.exception = {status: 503,
- headers: {"retry-after": BACKOFF}};
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- server.stop(() => {
- clean();
- deferred.resolve();
- });
- });
-
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- let ping = yield sync_and_validate_telem(true);
- deepEqual(ping.status.sync, PROLONGED_SYNC_FAILURE);
- deepEqual(ping.engines.find(e => e.failureReason).failureReason,
- { name: "httperror", code: 503 });
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_prolonged_server_maintenance_error(){
- // Test info/collections prolonged server maintenance errors are reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.info"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_prolonged_server_maintenance_error(){
- // Test meta/global prolonged server maintenance errors are reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.meta"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_download_crypto_keys_login_prolonged_server_maintenance_error(){
- // Test crypto/keys prolonged server maintenance errors are reported.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
- // Force re-download of keys
- Service.collectionKeys.clear();
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_upload_crypto_keys_login_prolonged_server_maintenance_error(){
- // Test crypto/keys prolonged server maintenance errors are reported.
- let server = sync_httpd_setup();
-
- // Start off with an empty account, do not upload a key.
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeServer_login_prolonged_server_maintenance_error(){
- // Test that we report prolonged server maintenance errors that occur whilst
- // wiping the server.
- let server = sync_httpd_setup();
-
- // Start off with an empty account, do not upload a key.
- yield configureIdentity({username: "broken.wipe"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_true(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- Service.sync();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeRemote_prolonged_server_maintenance_error(){
- // Test that we report prolonged server maintenance errors that occur whilst
- // wiping all remote devices.
- let server = sync_httpd_setup();
-
- server.registerPathHandler("/1.1/broken.wipe/storage/catapult", service_unavailable);
- yield configureIdentity({username: "broken.wipe"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
- generateAndUploadKeys();
-
- let engine = engineManager.get("catapult");
- engine.exception = null;
- engine.enabled = true;
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
- do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
- do_check_true(errorHandler.didReportProlongedError);
- server.stop(() => {
- clean();
- deferred.resolve();
- });
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- Svc.Prefs.set("firstSync", "wipeRemote");
- setLastSync(PROLONGED_ERROR_DURATION);
- let ping = yield sync_and_validate_telem(true);
- deepEqual(ping.failureReason, { name: "httperror", code: 503 });
- yield deferred.promise;
-});
-
-add_task(function* test_sync_syncAndReportErrors_server_maintenance_error() {
- // Test server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- const BACKOFF = 42;
- let engine = engineManager.get("catapult");
- engine.enabled = true;
- engine.exception = {status: 503,
- headers: {"retry-after": BACKOFF}};
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
- do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
- do_check_eq(Status.sync, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
- // Test info/collections server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.info"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
- // Test meta/global server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.meta"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
- // Force re-download of keys
- Service.collectionKeys.clear();
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
-
- // Start off with an empty account, do not upload a key.
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
-
- // Start off with an empty account, do not upload a key.
- yield configureIdentity({username: "broken.wipe"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeRemote_syncAndReportErrors_server_maintenance_error(){
- // Test that we report prolonged server maintenance errors that occur whilst
- // wiping all remote devices.
- let server = sync_httpd_setup();
-
- yield configureIdentity({username: "broken.wipe"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
- generateAndUploadKeys();
-
- let engine = engineManager.get("catapult");
- engine.exception = null;
- engine.enabled = true;
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, SYNC_FAILED);
- do_check_eq(Status.sync, SERVER_MAINTENANCE);
- do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- Svc.Prefs.set("firstSync", "wipeRemote");
- setLastSync(NON_PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_task(function* test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
- // Test prolonged server maintenance errors are
- // reported when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- const BACKOFF = 42;
- let engine = engineManager.get("catapult");
- engine.enabled = true;
- engine.exception = {status: 503,
- headers: {"retry-after": BACKOFF}};
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
- do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
- do_check_eq(Status.sync, SERVER_MAINTENANCE);
- // syncAndReportErrors means dontIgnoreErrors, which means
- // didReportProlongedError not touched.
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- // Test info/collections server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.info"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- // syncAndReportErrors means dontIgnoreErrors, which means
- // didReportProlongedError not touched.
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- // Test meta/global server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.meta"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- // syncAndReportErrors means dontIgnoreErrors, which means
- // didReportProlongedError not touched.
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
- yield setUp(server);
-
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
- // Force re-download of keys
- Service.collectionKeys.clear();
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- // syncAndReportErrors means dontIgnoreErrors, which means
- // didReportProlongedError not touched.
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
-
- // Start off with an empty account, do not upload a key.
- yield configureIdentity({username: "broken.keys"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- // syncAndReportErrors means dontIgnoreErrors, which means
- // didReportProlongedError not touched.
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- // Test crypto/keys server maintenance errors are reported
- // when calling syncAndReportErrors.
- let server = sync_httpd_setup();
-
- // Start off with an empty account, do not upload a key.
- yield configureIdentity({username: "broken.wipe"});
- Service.serverURL = server.baseURI + "/maintenance/";
- Service.clusterURL = server.baseURI + "/maintenance/";
-
- let backoffInterval;
- Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
- Svc.Obs.remove("weave:service:backoff:interval", observe);
- backoffInterval = subject;
- });
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
- Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
- do_check_true(Status.enforceBackoff);
- do_check_eq(backoffInterval, 42);
- do_check_eq(Status.service, LOGIN_FAILED);
- do_check_eq(Status.login, SERVER_MAINTENANCE);
- // syncAndReportErrors means dontIgnoreErrors, which means
- // didReportProlongedError not touched.
- do_check_false(errorHandler.didReportProlongedError);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_false(Status.enforceBackoff);
- do_check_eq(Status.service, STATUS_OK);
-
- setLastSync(PROLONGED_ERROR_DURATION);
- errorHandler.syncAndReportErrors();
- yield deferred.promise;
-});
-
-add_task(function* test_sync_engine_generic_fail() {
- let server = sync_httpd_setup();
-
-let engine = engineManager.get("catapult");
- engine.enabled = true;
- engine.sync = function sync() {
- Svc.Obs.notify("weave:engine:sync:error", ENGINE_UNKNOWN_FAIL, "catapult");
- };
-
- let log = Log.repository.getLogger("Sync.ErrorHandler");
- Svc.Prefs.set("log.appender.file.logOnError", true);
-
- do_check_eq(Status.engines["catapult"], undefined);
-
- let deferred = Promise.defer();
- // Don't wait for reset-file-log until the sync is underway.
- // This avoids us catching a delayed notification from an earlier test.
- Svc.Obs.add("weave:engine:sync:finish", function onEngineFinish() {
- Svc.Obs.remove("weave:engine:sync:finish", onEngineFinish);
-
- log.info("Adding reset-file-log observer.");
- Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
- Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
- // Put these checks here, not after sync(), so that we aren't racing the
- // log handler... which resets everything just a few lines below!
- _("Status.engines: " + JSON.stringify(Status.engines));
- do_check_eq(Status.engines["catapult"], ENGINE_UNKNOWN_FAIL);
- do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-
- // Test Error log was written on SYNC_FAILED_PARTIAL.
- let entries = logsdir.directoryEntries;
- do_check_true(entries.hasMoreElements());
- let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
- clean();
-
- let syncErrors = sumHistogram("WEAVE_ENGINE_SYNC_ERRORS", { key: "catapult" });
- do_check_true(syncErrors, 1);
-
- server.stop(() => {
- clean();
- deferred.resolve();
- });
- });
- });
-
- do_check_true(yield setUp(server));
- let ping = yield sync_and_validate_telem(true);
- deepEqual(ping.status.service, SYNC_FAILED_PARTIAL);
- deepEqual(ping.engines.find(e => e.status).status, ENGINE_UNKNOWN_FAIL);
-
- yield deferred.promise;
-});
-
-add_test(function test_logs_on_sync_error_despite_shouldReportError() {
- _("Ensure that an error is still logged when weave:service:sync:error " +
- "is notified, despite shouldReportError returning false.");
-
- let log = Log.repository.getLogger("Sync.ErrorHandler");
- Svc.Prefs.set("log.appender.file.logOnError", true);
- log.info("TESTING");
-
- // Ensure that we report no error.
- Status.login = MASTER_PASSWORD_LOCKED;
- do_check_false(errorHandler.shouldReportError());
-
- Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
- Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
- // Test that error log was written.
- let entries = logsdir.directoryEntries;
- do_check_true(entries.hasMoreElements());
- let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
- clean();
- run_next_test();
- });
- Svc.Obs.notify("weave:service:sync:error", {});
-});
-
-add_test(function test_logs_on_login_error_despite_shouldReportError() {
- _("Ensure that an error is still logged when weave:service:login:error " +
- "is notified, despite shouldReportError returning false.");
-
- let log = Log.repository.getLogger("Sync.ErrorHandler");
- Svc.Prefs.set("log.appender.file.logOnError", true);
- log.info("TESTING");
-
- // Ensure that we report no error.
- Status.login = MASTER_PASSWORD_LOCKED;
- do_check_false(errorHandler.shouldReportError());
-
- Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
- Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
- // Test that error log was written.
- let entries = logsdir.directoryEntries;
- do_check_true(entries.hasMoreElements());
- let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
- clean();
- run_next_test();
- });
- Svc.Obs.notify("weave:service:login:error", {});
-});
-
-// This test should be the last one since it monkeypatches the engine object
-// and we should only have one engine object throughout the file (bug 629664).
-add_task(function* test_engine_applyFailed() {
- let server = sync_httpd_setup();
-
- let engine = engineManager.get("catapult");
- engine.enabled = true;
- delete engine.exception;
- engine.sync = function sync() {
- Svc.Obs.notify("weave:engine:sync:applied", {newFailed:1}, "catapult");
- };
-
- let log = Log.repository.getLogger("Sync.ErrorHandler");
- Svc.Prefs.set("log.appender.file.logOnError", true);
-
- let deferred = Promise.defer();
- Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
- Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
- do_check_eq(Status.engines["catapult"], ENGINE_APPLY_FAIL);
- do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-
- // Test Error log was written on SYNC_FAILED_PARTIAL.
- let entries = logsdir.directoryEntries;
- do_check_true(entries.hasMoreElements());
- let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
- do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
- clean();
- server.stop(deferred.resolve);
- });
-
- do_check_eq(Status.engines["catapult"], undefined);
- do_check_true(yield setUp(server));
- Service.sync();
- yield deferred.promise;
-});
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_errorhandler_1.js
@@ -0,0 +1,913 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/engines/clients.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/keys.js");
+Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+var fakeServer = new SyncServer();
+fakeServer.start();
+
+do_register_cleanup(function() {
+ return new Promise(resolve => {
+ fakeServer.stop(resolve);
+ });
+});
+
+var fakeServerUrl = "http://localhost:" + fakeServer.port;
+
+const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
+
+const PROLONGED_ERROR_DURATION =
+ (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
+
+const NON_PROLONGED_ERROR_DURATION =
+ (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
+
+Service.engineManager.clear();
+
+function setLastSync(lastSyncValue) {
+ Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
+}
+
+var engineManager = Service.engineManager;
+engineManager.register(EHTestsCommon.CatapultEngine);
+
+// This relies on Service/ErrorHandler being a singleton. Fixing this will take
+// a lot of work.
+var errorHandler = Service.errorHandler;
+
+function run_test() {
+ initTestLogging("Trace");
+
+ Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
+
+ ensureLegacyIdentityManager();
+
+ run_next_test();
+}
+
+
+function clean() {
+ Service.startOver();
+ Status.resetSync();
+ Status.resetBackoff();
+ errorHandler.didReportProlongedError = false;
+}
+
+add_identity_test(this, function* test_401_logout() {
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+
+ // By calling sync, we ensure we're logged in.
+ yield sync_and_validate_telem();
+ do_check_eq(Status.sync, SYNC_SUCCEEDED);
+ do_check_true(Service.isLoggedIn);
+
+ let deferred = Promise.defer();
+ Svc.Obs.add("weave:service:sync:error", onSyncError);
+ function onSyncError() {
+ _("Got weave:service:sync:error in first sync.");
+ Svc.Obs.remove("weave:service:sync:error", onSyncError);
+
+ // Wait for the automatic next sync.
+ function onLoginError() {
+ _("Got weave:service:login:error in second sync.");
+ Svc.Obs.remove("weave:service:login:error", onLoginError);
+
+ let expected = isConfiguredWithLegacyIdentity() ?
+ LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR;
+
+ do_check_eq(Status.login, expected);
+ do_check_false(Service.isLoggedIn);
+
+ // Clean up.
+ Utils.nextTick(function () {
+ Service.startOver();
+ server.stop(deferred.resolve);
+ });
+ }
+ Svc.Obs.add("weave:service:login:error", onLoginError);
+ }
+
+ // Make sync fail due to login rejected.
+ yield configureIdentity({username: "janedoe"});
+ Service._updateCachedURLs();
+
+ _("Starting first sync.");
+ let ping = yield sync_and_validate_telem(true);
+ deepEqual(ping.failureReason, { name: "httperror", code: 401 });
+ _("First sync done.");
+ yield deferred.promise;
+});
+
+add_identity_test(this, function* test_credentials_changed_logout() {
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+
+ // By calling sync, we ensure we're logged in.
+ yield sync_and_validate_telem();
+ do_check_eq(Status.sync, SYNC_SUCCEEDED);
+ do_check_true(Service.isLoggedIn);
+
+ EHTestsCommon.generateCredentialsChangedFailure();
+
+ let ping = yield sync_and_validate_telem(true);
+ equal(ping.status.sync, CREDENTIALS_CHANGED);
+ deepEqual(ping.failureReason, {
+ name: "unexpectederror",
+ error: "Error: Aborting sync, remote setup failed"
+ });
+
+ do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+ do_check_false(Service.isLoggedIn);
+
+ // Clean up.
+ Service.startOver();
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ yield deferred.promise;
+});
+
+add_identity_test(this, function test_no_lastSync_pref() {
+ // Test reported error.
+ Status.resetSync();
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = CREDENTIALS_CHANGED;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test unreported error.
+ Status.resetSync();
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+
+});
+
+add_identity_test(this, function test_shouldReportError() {
+ Status.login = MASTER_PASSWORD_LOCKED;
+ do_check_false(errorHandler.shouldReportError());
+
+ // Give ourselves a clusterURL so that the temporary 401 no-error situation
+ // doesn't come into play.
+ Service.serverURL = fakeServerUrl;
+ Service.clusterURL = fakeServerUrl;
+
+ // Test dontIgnoreErrors, non-network, non-prolonged, login error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = LOGIN_FAILED_NO_PASSWORD;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = CREDENTIALS_CHANGED;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, non-network, prolonged, login error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = LOGIN_FAILED_NO_PASSWORD;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, non-network, prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = CREDENTIALS_CHANGED;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, network, non-prolonged, login error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, network, non-prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, network, prolonged, login error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test dontIgnoreErrors, network, prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+
+ // Test non-network, prolonged, login error reported
+ do_check_false(errorHandler.didReportProlongedError);
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = LOGIN_FAILED_NO_PASSWORD;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+
+ // Second time with prolonged error and without resetting
+ // didReportProlongedError, sync error should not be reported.
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = LOGIN_FAILED_NO_PASSWORD;
+ do_check_false(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+
+ // Test non-network, prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ errorHandler.didReportProlongedError = false;
+ Status.sync = CREDENTIALS_CHANGED;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
+
+ // Test network, prolonged, login error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
+
+ // Test network, prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
+
+ // Test non-network, non-prolonged, login error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = LOGIN_FAILED_NO_PASSWORD;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test non-network, non-prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.sync = CREDENTIALS_CHANGED;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test network, non-prolonged, login error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test network, non-prolonged, sync error reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+ do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test server maintenance, sync errors are not reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.sync = SERVER_MAINTENANCE;
+ do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test server maintenance, login errors are not reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = SERVER_MAINTENANCE;
+ do_check_false(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test prolonged, server maintenance, sync errors are reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.sync = SERVER_MAINTENANCE;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
+
+ // Test prolonged, server maintenance, login errors are reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = false;
+ Status.login = SERVER_MAINTENANCE;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_true(errorHandler.didReportProlongedError);
+ errorHandler.didReportProlongedError = false;
+
+ // Test dontIgnoreErrors, server maintenance, sync errors are reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = SERVER_MAINTENANCE;
+ do_check_true(errorHandler.shouldReportError());
+ // dontIgnoreErrors means we don't set didReportProlongedError
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test dontIgnoreErrors, server maintenance, login errors are reported
+ Status.resetSync();
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = SERVER_MAINTENANCE;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test dontIgnoreErrors, prolonged, server maintenance,
+ // sync errors are reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.sync = SERVER_MAINTENANCE;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+
+ // Test dontIgnoreErrors, prolonged, server maintenance,
+ // login errors are reported
+ Status.resetSync();
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.dontIgnoreErrors = true;
+ Status.login = SERVER_MAINTENANCE;
+ do_check_true(errorHandler.shouldReportError());
+ do_check_false(errorHandler.didReportProlongedError);
+});
+
+add_identity_test(this, function* test_shouldReportError_master_password() {
+ _("Test error ignored due to locked master password");
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+
+ // Monkey patch Service.verifyLogin to imitate
+ // master password being locked.
+ Service._verifyLogin = Service.verifyLogin;
+ Service.verifyLogin = function () {
+ Status.login = MASTER_PASSWORD_LOCKED;
+ return false;
+ };
+
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ Service.sync();
+ do_check_false(errorHandler.shouldReportError());
+
+ // Clean up.
+ Service.verifyLogin = Service._verifyLogin;
+ clean();
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ yield deferred.promise;
+});
+
+// Test that even if we don't have a cluster URL, a login failure due to
+// authentication errors is always reported.
+add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
+ // Ensure no clusterURL - any error not specific to login should not be reported.
+ Service.serverURL = "";
+ Service.clusterURL = "";
+
+ // Test explicit "login rejected" state.
+ Status.resetSync();
+ // If we have a LOGIN_REJECTED state, we always report the error.
+ Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+ do_check_true(errorHandler.shouldReportError());
+ // But any other status with a missing clusterURL is treated as a mid-sync
+ // 401 (ie, should be treated as a node reassignment)
+ Status.login = LOGIN_SUCCEEDED;
+ do_check_false(errorHandler.shouldReportError());
+});
+
+// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
+// an fxaccounts environment?
+add_task(function* test_login_syncAndReportErrors_non_network_error() {
+ // Test non-network errors are reported
+ // when calling syncAndReportErrors
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+ Service.identity.basicPassword = null;
+
+ let deferred = Promise.defer();
+ Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+ Svc.Obs.remove("weave:ui:login:error", onSyncError);
+ do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+
+ clean();
+ server.stop(deferred.resolve);
+ });
+
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ errorHandler.syncAndReportErrors();
+ yield deferred.promise;
+});
+
+add_identity_test(this, function* test_sync_syncAndReportErrors_non_network_error() {
+ // Test non-network errors are reported
+ // when calling syncAndReportErrors
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+
+ // By calling sync, we ensure we're logged in.
+ Service.sync();
+ do_check_eq(Status.sync, SYNC_SUCCEEDED);
+ do_check_true(Service.isLoggedIn);
+
+ EHTestsCommon.generateCredentialsChangedFailure();
+
+ let deferred = Promise.defer();
+ Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+ Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+ do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+ // If we clean this tick, telemetry won't get the right error
+ server.stop(() => {
+ clean();
+ deferred.resolve();
+ });
+ });
+
+ setLastSync(NON_PROLONGED_ERROR_DURATION);
+ let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
+ equal(ping.status.sync, CREDENTIALS_CHANGED);
+ deepEqual(ping.failureReason, {
+ name: "unexpectederror",
+ error: "Error: Aborting sync, remote setup failed"
+ });
+ yield deferred.promise;
+});
+
+// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
+// an fxaccounts environment?
+add_task(function* test_login_syncAndReportErrors_prolonged_non_network_error() {
+ // Test prolonged, non-network errors are
+ // reported when calling syncAndReportErrors.
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+ Service.identity.basicPassword = null;
+
+ let deferred = Promise.defer();
+ Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+ Svc.Obs.remove("weave:ui:login:error", onSyncError);
+ do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+
+ clean();
+ server.stop(deferred.resolve);
+ });
+
+ setLastSync(PROLONGED_ERROR_DURATION);
+ errorHandler.syncAndReportErrors();
+ yield deferred.promise;
+});
+
+add_identity_test(this, function* test_sync_syncAndReportErrors_prolonged_non_network_error() {
+ // Test prolonged, non-network errors are
+ // reported when calling syncAndReportErrors.
+ let server = EHTestsCommon.sync_httpd_setup();
+ yield EHTestsCommon.setUp(server);
+
+ // By calling sync, we ensure we're logged in.
+ Service.sync();
+ do_check_eq(Status.sync, SYNC_SUCCEEDED);
+ do_check_true(Service.isLoggedIn);
+
+ EHTestsCommon.generateCredentialsChangedFailure();
+
+ let deferred = Promise.defer();
+ Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+ Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+ do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+ // If we clean this tick, telemetry won't get the right error
+ server.stop(() => {
+ clean();
+ deferred.resolve();
+ });
+ });
+
+ setLastSync(PROLONGED_ERROR_DURATION);
+ let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);