Merge mozilla-central to mozilla-inbound
@ -7,6 +7,9 @@ var gTestBrowser = null;
|
||||
var config = {};
|
||||
|
||||
add_task(function* () {
|
||||
Services.prefs.setIntPref("media.gmp.log.level", 0);
|
||||
Services.prefs.setBoolPref("extensions.logging.enabled", true);
|
||||
|
||||
// The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
|
||||
// crash reports. This test needs them enabled. The test also needs a mock
|
||||
// report server, and fortunately one is already set up by toolkit/
|
||||
@ -26,6 +29,8 @@ add_task(function* () {
|
||||
Services.prefs.setIntPref("dom.ipc.plugins.timeoutSecs", 0);
|
||||
|
||||
registerCleanupFunction(Task.async(function*() {
|
||||
Services.prefs.clearUserPref("extensions.logging.enabled");
|
||||
Services.prefs.clearUserPref("media.gmp.log.level");
|
||||
Services.prefs.clearUserPref("dom.ipc.plugins.timeoutSecs");
|
||||
env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
|
||||
env.set("MOZ_CRASHREPORTER_URL", serverUrl);
|
||||
|
@ -448,14 +448,9 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
||||
let options = {
|
||||
js: [],
|
||||
css: [],
|
||||
};
|
||||
|
||||
// We need to send the inner window ID to make sure we only
|
||||
// execute the script if the window is currently navigated to
|
||||
// the document that we expect.
|
||||
//
|
||||
// TODO: When we add support for callbacks, non-matching
|
||||
// window IDs and insufficient permissions need to result in a
|
||||
// callback with |lastError| set.
|
||||
let recipient = {
|
||||
innerWindowID: tab.linkedBrowser.innerWindowID,
|
||||
};
|
||||
|
||||
@ -487,17 +482,21 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
|
||||
if (details.runAt !== null) {
|
||||
options.run_at = details.runAt;
|
||||
}
|
||||
mm.sendAsyncMessage("Extension:Execute",
|
||||
{extensionId: extension.id, options});
|
||||
|
||||
// TODO: Call the callback with the result (which is what???).
|
||||
// TODO: Set lastError.
|
||||
context.sendMessage(mm, "Extension:Execute", { options }, recipient)
|
||||
.then(result => {
|
||||
if (callback) {
|
||||
runSafe(context, callback, result);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
executeScript: function(tabId, details, callback) {
|
||||
self.tabs._execute(tabId, details, "js", callback);
|
||||
},
|
||||
|
||||
insertCss: function(tabId, details, callback) {
|
||||
insertCSS: function(tabId, details, callback) {
|
||||
self.tabs._execute(tabId, details, "css", callback);
|
||||
},
|
||||
|
||||
|
@ -20,8 +20,10 @@ support-files =
|
||||
[browser_ext_popup_api_injection.js]
|
||||
[browser_ext_contextMenus.js]
|
||||
[browser_ext_getViews.js]
|
||||
[browser_ext_tabs_executeScript.js]
|
||||
[browser_ext_tabs_executeScript_good.js]
|
||||
[browser_ext_tabs_executeScript_bad.js]
|
||||
[browser_ext_tabs_insertCSS.js]
|
||||
[browser_ext_tabs_query.js]
|
||||
[browser_ext_tabs_getCurrent.js]
|
||||
[browser_ext_tabs_create.js]
|
||||
|
@ -0,0 +1,66 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(function* testExecuteScript() {
|
||||
let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {});
|
||||
|
||||
let messageManagersSize = MessageChannel.messageManagers.size;
|
||||
let responseManagersSize = MessageChannel.responseManagers.size;
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true);
|
||||
|
||||
function background() {
|
||||
browser.tabs.executeScript({
|
||||
file: "script.js",
|
||||
code: "42",
|
||||
}, result => {
|
||||
browser.test.assertEq(42, result, "Expected callback result");
|
||||
browser.test.sendMessage("got result", result);
|
||||
});
|
||||
|
||||
browser.tabs.executeScript({
|
||||
file: "script2.js",
|
||||
}, result => {
|
||||
browser.test.assertEq(27, result, "Expected callback result");
|
||||
browser.test.sendMessage("got callback", result);
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener(message => {
|
||||
browser.test.assertEq("script ran", message, "Expected runtime message");
|
||||
browser.test.sendMessage("got message", message);
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"permissions": ["http://mochi.test/"],
|
||||
},
|
||||
|
||||
background,
|
||||
|
||||
files: {
|
||||
"script.js": function() {
|
||||
browser.runtime.sendMessage("script ran");
|
||||
},
|
||||
|
||||
"script2.js": "27",
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield extension.awaitMessage("got result");
|
||||
yield extension.awaitMessage("got callback");
|
||||
yield extension.awaitMessage("got message");
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
// Make sure that we're not holding on to references to closed message
|
||||
// managers.
|
||||
is(MessageChannel.messageManagers.size, messageManagersSize, "Message manager count");
|
||||
is(MessageChannel.responseManagers.size, responseManagersSize, "Response manager count");
|
||||
is(MessageChannel.pendingResponses.size, 0, "Pending response count");
|
||||
});
|
@ -0,0 +1,106 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(function* testExecuteScript() {
|
||||
let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {});
|
||||
|
||||
let messageManagersSize = MessageChannel.messageManagers.size;
|
||||
let responseManagersSize = MessageChannel.responseManagers.size;
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true);
|
||||
|
||||
function background() {
|
||||
let promises = [
|
||||
{
|
||||
background: "rgb(0, 0, 0)",
|
||||
foreground: "rgb(255, 192, 203)",
|
||||
promise: resolve => {
|
||||
browser.tabs.insertCSS({
|
||||
file: "file1.css",
|
||||
code: "* { background: black }",
|
||||
}, result => {
|
||||
browser.test.assertEq(undefined, result, "Expected callback result");
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
background: "rgb(0, 0, 0)",
|
||||
foreground: "rgb(0, 113, 4)",
|
||||
promise: resolve => {
|
||||
browser.tabs.insertCSS({
|
||||
file: "file2.css",
|
||||
}, result => {
|
||||
browser.test.assertEq(undefined, result, "Expected callback result");
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
background: "rgb(42, 42, 42)",
|
||||
foreground: "rgb(0, 113, 4)",
|
||||
promise: resolve => {
|
||||
browser.tabs.insertCSS({
|
||||
code: "* { background: rgb(42, 42, 42) }",
|
||||
}, result => {
|
||||
browser.test.assertEq(undefined, result, "Expected callback result");
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function checkCSS() {
|
||||
let computedStyle = window.getComputedStyle(document.body);
|
||||
return [computedStyle.backgroundColor, computedStyle.color];
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (!promises.length) {
|
||||
browser.test.notifyPass("insertCSS");
|
||||
return;
|
||||
}
|
||||
|
||||
let { promise, background, foreground } = promises.shift();
|
||||
new Promise(promise).then(() => {
|
||||
browser.tabs.executeScript({
|
||||
code: `(${checkCSS})()`,
|
||||
}, result => {
|
||||
browser.test.assertEq(background, result[0], "Expected background color");
|
||||
browser.test.assertEq(foreground, result[1], "Expected foreground color");
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
"permissions": ["http://mochi.test/"],
|
||||
},
|
||||
|
||||
background,
|
||||
|
||||
files: {
|
||||
"file1.css": "* { color: pink }",
|
||||
"file2.css": "* { color: rgb(0, 113, 4) }",
|
||||
},
|
||||
});
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
yield extension.awaitFinish("insertCSS");
|
||||
|
||||
yield extension.unload();
|
||||
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
// Make sure that we're not holding on to references to closed message
|
||||
// managers.
|
||||
is(MessageChannel.messageManagers.size, messageManagersSize, "Message manager count");
|
||||
is(MessageChannel.responseManagers.size, responseManagersSize, "Response manager count");
|
||||
is(MessageChannel.pendingResponses.size, 0, "Pending response count");
|
||||
});
|
@ -213,15 +213,15 @@
|
||||
</vbox>
|
||||
</hbox>
|
||||
<label class="fxaMobilePromo">
|
||||
&mobilePromo2.start;<!-- We put these comments to avoid inserting white spaces
|
||||
&mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces
|
||||
--><label id="fxaMobilePromo-android"
|
||||
class="androidLink text-link"><!--
|
||||
-->&mobilePromo2.androidLink;</label><!--
|
||||
-->&mobilePromo2.iOSBefore;<!--
|
||||
-->&mobilePromo3.androidLink;</label><!--
|
||||
-->&mobilePromo3.iOSBefore;<!--
|
||||
--><label id="fxaMobilePromo-ios"
|
||||
class="iOSLink text-link"><!--
|
||||
-->&mobilePromo2.iOSLink;</label><!--
|
||||
-->&mobilePromo2.end;
|
||||
-->&mobilePromo3.iOSLink;</label><!--
|
||||
-->&mobilePromo3.end;
|
||||
</label>
|
||||
</vbox>
|
||||
|
||||
@ -347,15 +347,15 @@
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<label class="fxaMobilePromo">
|
||||
&mobilePromo2.start;<!-- We put these comments to avoid inserting white spaces
|
||||
&mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces
|
||||
--><label class="androidLink text-link"
|
||||
href="https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=sync-preferences"><!--
|
||||
-->&mobilePromo2.androidLink;</label><!--
|
||||
-->&mobilePromo2.iOSBefore;<!--
|
||||
-->&mobilePromo3.androidLink;</label><!--
|
||||
-->&mobilePromo3.iOSBefore;<!--
|
||||
--><label class="iOSLink text-link"
|
||||
href="https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=sync-preferences"><!--
|
||||
-->&mobilePromo2.iOSLink;</label><!--
|
||||
-->&mobilePromo2.end;
|
||||
-->&mobilePromo3.iOSLink;</label><!--
|
||||
-->&mobilePromo3.end;
|
||||
</label>
|
||||
<vbox id="tosPP-small" align="start">
|
||||
<label id="tosPP-small-ToS" class="text-link">
|
||||
|
@ -220,17 +220,6 @@ if (typeof Mozilla == 'undefined') {
|
||||
});
|
||||
};
|
||||
|
||||
Mozilla.UITour.startUrlbarCapture = function(text, url) {
|
||||
_sendEvent('startUrlbarCapture', {
|
||||
text: text,
|
||||
url: url
|
||||
});
|
||||
};
|
||||
|
||||
Mozilla.UITour.endUrlbarCapture = function() {
|
||||
_sendEvent('endUrlbarCapture');
|
||||
};
|
||||
|
||||
Mozilla.UITour.getConfiguration = function(configName, callback) {
|
||||
_sendEvent('getConfiguration', {
|
||||
callbackID: _waitForCallback(callback),
|
||||
|
@ -43,7 +43,6 @@ const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs";
|
||||
const PREF_READERVIEW_TRIGGER = "browser.uitour.readerViewTrigger";
|
||||
|
||||
const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([
|
||||
"endUrlbarCapture",
|
||||
"forceShowReaderIcon",
|
||||
"getConfiguration",
|
||||
"getTreatmentTag",
|
||||
@ -90,7 +89,6 @@ this.UITour = {
|
||||
pageIDSourceBrowsers: new WeakMap(),
|
||||
/* Map from browser chrome windows to a Set of <browser>s in which a tour is open (both visible and hidden) */
|
||||
tourBrowsersByWindow: new WeakMap(),
|
||||
urlbarCapture: new WeakMap(),
|
||||
appMenuOpenForAnnotation: new Set(),
|
||||
availableTargetsCache: new WeakMap(),
|
||||
clearAvailableTargetsCache() {
|
||||
@ -577,41 +575,6 @@ this.UITour = {
|
||||
break;
|
||||
}
|
||||
|
||||
case "startUrlbarCapture": {
|
||||
if (typeof data.text != "string" || !data.text ||
|
||||
typeof data.url != "string" || !data.url) {
|
||||
log.warn("startUrlbarCapture: Text or URL not specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
let uri = null;
|
||||
try {
|
||||
uri = Services.io.newURI(data.url, null, null);
|
||||
} catch (e) {
|
||||
log.warn("startUrlbarCapture: Malformed URL specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
let secman = Services.scriptSecurityManager;
|
||||
let contentDocument = browser.contentWindow.document;
|
||||
let principal = contentDocument.nodePrincipal;
|
||||
let flags = secman.DISALLOW_INHERIT_PRINCIPAL;
|
||||
try {
|
||||
secman.checkLoadURIWithPrincipal(principal, uri, flags);
|
||||
} catch (e) {
|
||||
log.warn("startUrlbarCapture: Orginating page doesn't have permission to open specified URL");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.startUrlbarCapture(window, data.text, data.url);
|
||||
break;
|
||||
}
|
||||
|
||||
case "endUrlbarCapture": {
|
||||
this.endUrlbarCapture(window);
|
||||
break;
|
||||
}
|
||||
|
||||
case "getConfiguration": {
|
||||
if (typeof data.configuration != "string") {
|
||||
log.warn("getConfiguration: No configuration option specified");
|
||||
@ -698,8 +661,8 @@ this.UITour = {
|
||||
targetPromise.then(target => {
|
||||
let searchbar = target.node;
|
||||
searchbar.value = data.term;
|
||||
searchbar.inputChanged();
|
||||
}).then(null, Cu.reportError);
|
||||
searchbar.updateGoButtonVisibility();
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@ -806,14 +769,6 @@ this.UITour = {
|
||||
this.teardownTourForWindow(window);
|
||||
break;
|
||||
}
|
||||
|
||||
case "input": {
|
||||
if (aEvent.target.id == "urlbar") {
|
||||
let window = aEvent.target.ownerDocument.defaultView;
|
||||
this.handleUrlbarInput(window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -903,7 +858,6 @@ this.UITour = {
|
||||
controlCenterPanel.removeEventListener("popuphidden", this.onPanelHidden);
|
||||
controlCenterPanel.removeEventListener("popuphiding", this.hideControlCenterAnnotations);
|
||||
|
||||
this.endUrlbarCapture(aWindow);
|
||||
this.resetTheme();
|
||||
|
||||
// If there are no more tour tabs left in the window, teardown the tour for the whole window.
|
||||
@ -1763,41 +1717,6 @@ this.UITour = {
|
||||
aPanel.hidden = false;
|
||||
},
|
||||
|
||||
startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {
|
||||
let urlbar = aWindow.document.getElementById("urlbar");
|
||||
this.urlbarCapture.set(aWindow, {
|
||||
expected: aExpectedText.toLocaleLowerCase(),
|
||||
url: aUrl
|
||||
});
|
||||
urlbar.addEventListener("input", this);
|
||||
},
|
||||
|
||||
endUrlbarCapture: function(aWindow) {
|
||||
let urlbar = aWindow.document.getElementById("urlbar");
|
||||
urlbar.removeEventListener("input", this);
|
||||
this.urlbarCapture.delete(aWindow);
|
||||
},
|
||||
|
||||
handleUrlbarInput: function(aWindow) {
|
||||
if (!this.urlbarCapture.has(aWindow))
|
||||
return;
|
||||
|
||||
let urlbar = aWindow.document.getElementById("urlbar");
|
||||
|
||||
let {expected, url} = this.urlbarCapture.get(aWindow);
|
||||
|
||||
if (urlbar.value.toLocaleLowerCase().localeCompare(expected) != 0)
|
||||
return;
|
||||
|
||||
urlbar.handleRevert();
|
||||
|
||||
let tab = aWindow.gBrowser.addTab(url, {
|
||||
owner: aWindow.gBrowser.selectedTab,
|
||||
relatedToCurrent: true
|
||||
});
|
||||
aWindow.gBrowser.selectedTab = tab;
|
||||
},
|
||||
|
||||
getConfiguration: function(aMessageManager, aWindow, aConfiguration, aCallbackID) {
|
||||
switch (aConfiguration) {
|
||||
case "appinfo":
|
||||
|
@ -91,18 +91,18 @@ both, to better adapt this sentence to their language.
|
||||
|
||||
<!ENTITY signedIn.engines.label "Sync across all devices">
|
||||
|
||||
<!-- LOCALIZATION NOTE (mobilePromo2.*): the following strings will be used to
|
||||
<!-- LOCALIZATION NOTE (mobilePromo3.*): the following strings will be used to
|
||||
create a single sentence with active links.
|
||||
The resulting sentence in English is: "Sync to your mobile device.
|
||||
Download Firefox for Android or Firefox for iOS." -->
|
||||
The resulting sentence in English is: "Download Firefox for
|
||||
Android or iOS to sync with your mobile device." -->
|
||||
|
||||
<!ENTITY mobilePromo2.start "Sync to your mobile device. Download ">
|
||||
<!-- LOCALIZATION NOTE (mobilePromo2.androidLink): This is a link title that links to https://www.mozilla.org/firefox/android/ -->
|
||||
<!ENTITY mobilePromo2.androidLink "Firefox for Android">
|
||||
<!ENTITY mobilePromo3.start "Download Firefox for ">
|
||||
<!-- LOCALIZATION NOTE (mobilePromo3.androidLink): This is a link title that links to https://www.mozilla.org/firefox/android/ -->
|
||||
<!ENTITY mobilePromo3.androidLink "Android">
|
||||
|
||||
<!-- LOCALIZATION NOTE (mobilePromo2.iOSBefore): This is text displayed between mobilePromo2.androidLink and mobilePromo2.iosLink -->
|
||||
<!ENTITY mobilePromo2.iOSBefore " or ">
|
||||
<!-- LOCALIZATION NOTE (mobilePromo2.iOSLink): This is a link title that links to https://www.mozilla.org/firefox/ios/ -->
|
||||
<!ENTITY mobilePromo2.iOSLink "Firefox for iOS">
|
||||
<!-- LOCALIZATION NOTE (mobilePromo3.iOSBefore): This is text displayed between mobilePromo3.androidLink and mobilePromo3.iosLink -->
|
||||
<!ENTITY mobilePromo3.iOSBefore " or ">
|
||||
<!-- LOCALIZATION NOTE (mobilePromo3.iOSLink): This is a link title that links to https://www.mozilla.org/firefox/ios/ -->
|
||||
<!ENTITY mobilePromo3.iOSLink "iOS">
|
||||
|
||||
<!ENTITY mobilePromo2.end ".">
|
||||
<!ENTITY mobilePromo3.end " to sync with your mobile device.">
|
||||
|
@ -96,7 +96,6 @@ body {
|
||||
|
||||
/* Advanced section is hidden via inline styles until the link is clicked */
|
||||
#advancedPanel {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
color: var(--in-content-text-color);
|
||||
border: 1px lightgray solid;
|
||||
|
@ -154,7 +154,6 @@ div#weakCryptoAdvancedPanel {
|
||||
padding: 0 12px 12px 12px;
|
||||
box-shadow: 0 0 4px #ddd;
|
||||
font-size: 0.9em;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#overrideWeakCryptoPanel {
|
||||
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 468 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 719 B |
BIN
browser/themes/shared/fxa/ios.png
Normal file
After Width: | Height: | Size: 711 B |
BIN
browser/themes/shared/fxa/ios@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
@ -563,7 +563,8 @@ description > html|a {
|
||||
}
|
||||
|
||||
.fxaMobilePromo {
|
||||
margin-bottom: 31px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
#fxaLoginRejectedWarning {
|
||||
@ -576,9 +577,22 @@ description > html|a {
|
||||
margin-bottom: 27.5px;
|
||||
}
|
||||
|
||||
.androidLink {
|
||||
background-image: url("chrome://browser/skin/fxa/android.png");
|
||||
}
|
||||
|
||||
.iOSLink {
|
||||
background-image: url("chrome://browser/skin/fxa/ios.png");
|
||||
}
|
||||
|
||||
.androidLink,
|
||||
.iOSLink {
|
||||
margin: 0;
|
||||
margin: 0 0 0 2px;
|
||||
padding-left: 28px;
|
||||
padding-top: 6px;
|
||||
height: 28px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 24px 28px;
|
||||
}
|
||||
|
||||
#tosPP-small {
|
||||
@ -587,6 +601,12 @@ description > html|a {
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
.androidLink {
|
||||
background-image: url("chrome://browser/skin/fxa/android@2x.png");
|
||||
}
|
||||
.iOSLink {
|
||||
background-image: url("chrome://browser/skin/fxa/ios@2x.png");
|
||||
}
|
||||
.fxaSyncIllustration {
|
||||
list-style-image: url(chrome://browser/skin/fxa/sync-illustration@2x.png)
|
||||
}
|
||||
|
@ -81,6 +81,8 @@
|
||||
skin/classic/browser/fxa/sync-illustration.svg (../shared/fxa/sync-illustration.svg)
|
||||
skin/classic/browser/fxa/android.png (../shared/fxa/android.png)
|
||||
skin/classic/browser/fxa/android@2x.png (../shared/fxa/android@2x.png)
|
||||
skin/classic/browser/fxa/ios.png (../shared/fxa/ios.png)
|
||||
skin/classic/browser/fxa/ios@2x.png (../shared/fxa/ios@2x.png)
|
||||
skin/classic/browser/syncedtabs/twisty-closed.svg (../shared/syncedtabs/twisty-closed.svg)
|
||||
skin/classic/browser/syncedtabs/twisty-open.svg (../shared/syncedtabs/twisty-open.svg)
|
||||
skin/classic/browser/search-pref.png (../shared/search/search-pref.png)
|
||||
|
@ -47,8 +47,6 @@
|
||||
<div class="header">
|
||||
<h1 class="header-name">&aboutDebugging.workers;</h1>
|
||||
</div>
|
||||
<input id="enable-worker-debugging" type="checkbox" data-pref="devtools.debugger.workers"/>
|
||||
<label for="enable-worker-debugging" title="&options.enableWorkers.tooltip;">&options.enableWorkers.label;</label>
|
||||
<div id="workers"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -77,3 +77,9 @@
|
||||
.gcli-row-out .nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gcli-mdn-url {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -145,5 +145,6 @@
|
||||
<splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
|
||||
<box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
|
||||
</vbox>
|
||||
<tooltip id="aHTMLTooltip" page="true" />
|
||||
</notificationbox>
|
||||
</window>
|
||||
|
@ -314,6 +314,7 @@ devtools.jar:
|
||||
skin/images/tool-memory-active.svg (themes/images/tool-memory-active.svg)
|
||||
skin/images/close.png (themes/images/close.png)
|
||||
skin/images/close@2x.png (themes/images/close@2x.png)
|
||||
skin/images/clear.svg (themes/images/clear.svg)
|
||||
skin/images/vview-delete.png (themes/images/vview-delete.png)
|
||||
skin/images/vview-delete@2x.png (themes/images/vview-delete@2x.png)
|
||||
skin/images/vview-edit.png (themes/images/vview-edit.png)
|
||||
|
@ -84,9 +84,9 @@ take-snapshot=Take snapshot
|
||||
# initiates importing a snapshot.
|
||||
import-snapshot=Import…
|
||||
|
||||
# LOCALIZATION NOTE (clear-snapshots): The label describing the button that clears
|
||||
# existing snapshot.
|
||||
clear-snapshots=Clear
|
||||
# LOCALIZATION NOTE (clear-snapshots.tooltip): The tooltip for the button that
|
||||
# deletes existing snapshot.
|
||||
clear-snapshots.tooltip=Delete all snapshots
|
||||
|
||||
# LOCALIZATION NOTE (diff-snapshots): The label for the button that initiates
|
||||
# selecting two snapshots to diff with each other.
|
||||
|
@ -196,11 +196,10 @@ const Toolbar = module.exports = createClass({
|
||||
|
||||
dom.button({
|
||||
id: "clear-snapshots",
|
||||
className: "devtools-toolbarbutton clear-snapshots devtools-button",
|
||||
className: "clear-snapshots devtools-button",
|
||||
onClick: onClearSnapshotsClick,
|
||||
title: L10N.getStr("clear-snapshots"),
|
||||
"data-text-only": true,
|
||||
}, L10N.getStr("clear-snapshots"))
|
||||
title: L10N.getStr("clear-snapshots.tooltip")
|
||||
})
|
||||
),
|
||||
|
||||
dom.label(
|
||||
|
@ -29,8 +29,11 @@ module.exports = createClass({
|
||||
// handles, etc.
|
||||
return dom.div(
|
||||
{
|
||||
className: "viewport",
|
||||
className: "viewport"
|
||||
},
|
||||
dom.div({
|
||||
className: "viewport-header",
|
||||
}),
|
||||
Browser({
|
||||
location,
|
||||
width: viewport.width,
|
||||
|
@ -1,6 +1,33 @@
|
||||
/* TODO: May break up into component local CSS. Pending future discussions by
|
||||
* React component group on how to best handle CSS. */
|
||||
|
||||
iframe {
|
||||
html, body, #app, #viewports {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#viewports {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewport Container
|
||||
*/
|
||||
|
||||
.viewport {
|
||||
border: 1px solid var(--theme-splitter-color);
|
||||
box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
|
||||
}
|
||||
|
||||
.viewport-header {
|
||||
background-color: var(--theme-toolbar-background);
|
||||
border-bottom: 1px solid var(--theme-splitter-color);
|
||||
color: var(--theme-body-color-alt);
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.browser {
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ var XHR_CSS_URL = "https://developer.mozilla.org/en-US/docs/Web/CSS/";
|
||||
const PAGE_LINK_PARAMS = "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default"
|
||||
// URL for the page link omits locale, so a locale-specific page will be loaded
|
||||
var PAGE_LINK_URL = "https://developer.mozilla.org/docs/Web/CSS/";
|
||||
exports.PAGE_LINK_URL = PAGE_LINK_URL;
|
||||
|
||||
const BROWSER_WINDOW = 'navigator:browser';
|
||||
|
||||
|
6
devtools/client/themes/images/clear.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#babec3">
|
||||
<path d="M8,1.1c-3.8,0-6.9,3-6.9,6.9s3,6.9,6.9,6.9s6.9-3,6.9-6.9S11.8,1.1,8,1.1z M13.1,7.9c0,1.1-0.4,2.1-1,3L5,3.8 c0.8-0.6,1.9-1,3-1C10.8,2.8,13.1,5.1,13.1,7.9z M2.9,7.9c0-1.1,0.3-2.1,0.9-2.9l7.1,7.1C10.1,12.7,9.1,13,8,13 C5.2,13,2.9,10.7,2.9,7.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 571 B |
@ -102,6 +102,11 @@ html, body, #app, #memory-tool {
|
||||
#take-snapshot::before {
|
||||
background-image: url(images/command-screenshot.png);
|
||||
}
|
||||
|
||||
#clear-snapshots::before {
|
||||
background-image: url(chrome://devtools/skin/images/clear.svg);
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
#take-snapshot::before {
|
||||
background-image: url(images/command-screenshot@2x.png);
|
||||
@ -233,6 +238,7 @@ html, body, #app, #memory-tool {
|
||||
* (https://drafts.csswg.org/css-flexbox/#min-size-auto)
|
||||
*/
|
||||
min-width: 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
#heap-view > .heap-view-panel {
|
||||
@ -294,7 +300,6 @@ html, body, #app, #memory-tool {
|
||||
text-overflow: ellipsis;
|
||||
line-height: var(--heap-tree-header-height);
|
||||
justify-content: center;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@ -317,6 +322,7 @@ html, body, #app, #memory-tool {
|
||||
|
||||
.tree-node {
|
||||
height: var(--heap-tree-row-height);
|
||||
line-height: var(--heap-tree-row-height);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -363,6 +369,12 @@ html, body, #app, #memory-tool {
|
||||
.heap-tree-item-bytes,
|
||||
.heap-tree-item-total-bytes {
|
||||
width: 10%;
|
||||
/*
|
||||
* Provision for up to :
|
||||
* - 12 characters for the number part (10s of GB and spaces every 3 digits)
|
||||
* - 4 chars for the percent part (the maximum length string is "100%")
|
||||
*/
|
||||
min-width: 16ch;
|
||||
}
|
||||
|
||||
.heap-tree-item-name {
|
||||
@ -410,7 +422,7 @@ html, body, #app, #memory-tool {
|
||||
}
|
||||
|
||||
.heap-tree-percent {
|
||||
width: 2.5em;
|
||||
width: 4ch;
|
||||
}
|
||||
|
||||
.heap-tree-item.focused .heap-tree-number,
|
||||
@ -487,9 +499,9 @@ html, body, #app, #memory-tool {
|
||||
}
|
||||
|
||||
.no-allocation-stacks {
|
||||
border-color: var(--theme-splitter-color);
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
border-color: var(--theme-splitter-color);
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ add_task(function* () {
|
||||
|
||||
function reload(browser) {
|
||||
let loaded = loadBrowser(browser);
|
||||
content.location.reload();
|
||||
browser.reload();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,7 @@ exports.devtoolsModules = [
|
||||
"devtools/shared/gcli/commands/inject",
|
||||
"devtools/shared/gcli/commands/jsb",
|
||||
"devtools/shared/gcli/commands/listen",
|
||||
"devtools/shared/gcli/commands/mdn",
|
||||
"devtools/shared/gcli/commands/measure",
|
||||
"devtools/shared/gcli/commands/media",
|
||||
"devtools/shared/gcli/commands/pagemod",
|
||||
|
76
devtools/shared/gcli/commands/mdn.js
Normal file
@ -0,0 +1,76 @@
|
||||
/* 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 l10n = require("gcli/l10n");
|
||||
const {
|
||||
getCssDocs,
|
||||
PAGE_LINK_URL
|
||||
} = require("devtools/client/shared/widgets/MdnDocsWidget");
|
||||
|
||||
exports.items = [{
|
||||
name: "mdn",
|
||||
description: l10n.lookup("mdnDesc")
|
||||
}, {
|
||||
item: "command",
|
||||
runAt: "client",
|
||||
name: "mdn css",
|
||||
description: l10n.lookup("mdnCssDesc"),
|
||||
returnType: "cssPropertyOutput",
|
||||
params: [{
|
||||
name: "property",
|
||||
type: { name: "string" },
|
||||
defaultValue: null,
|
||||
description: l10n.lookup("mdnCssProp")
|
||||
}],
|
||||
exec: function(args) {
|
||||
return getCssDocs(args.property).then(result => {
|
||||
return {
|
||||
data: result,
|
||||
url: PAGE_LINK_URL + args.property,
|
||||
property: args.property
|
||||
};
|
||||
}, error => {
|
||||
return { error, property: args.property };
|
||||
});
|
||||
}
|
||||
}, {
|
||||
item: "converter",
|
||||
from: "cssPropertyOutput",
|
||||
to: "dom",
|
||||
exec: function(result, context) {
|
||||
let propertyName = result.property;
|
||||
|
||||
let document = context.document;
|
||||
let root = document.createElement("div");
|
||||
|
||||
if (result.error) {
|
||||
// The css property specified doesn't exist.
|
||||
root.appendChild(document.createTextNode(
|
||||
l10n.lookupFormat("mdnCssPropertyNotFound", [ propertyName ]) +
|
||||
" (" + result.error + ")"));
|
||||
} else {
|
||||
let title = document.createElement("h2");
|
||||
title.textContent = propertyName;
|
||||
root.appendChild(title);
|
||||
|
||||
let link = document.createElement("p");
|
||||
link.classList.add("gcli-mdn-url");
|
||||
link.textContent = l10n.lookup("mdnCssVisitPage");
|
||||
root.appendChild(link);
|
||||
|
||||
link.addEventListener("click", () => {
|
||||
let gBrowser = context.environment.chromeWindow.gBrowser;
|
||||
gBrowser.selectedTab = gBrowser.addTab(result.url);
|
||||
});
|
||||
|
||||
let summary = document.createElement("p");
|
||||
summary.textContent = result.data.summary;
|
||||
root.appendChild(summary);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
}];
|
@ -1612,6 +1612,24 @@ folderInvalidPath=Please enter a valid path
|
||||
# The argument (%1$S) is the folder path.
|
||||
folderOpenDirResult=Opened %1$S
|
||||
|
||||
# LOCALIZATION NOTE (mdnDesc) A very short string used to describe the
|
||||
# use of 'mdn' command.
|
||||
mdnDesc=Retrieve documentation from MDN
|
||||
# LOCALIZATION NOTE (mdnCssDesc) A very short string used to describe the
|
||||
# result of the 'mdn css' commmand.
|
||||
mdnCssDesc=Retrieve documentation about a given CSS property name from MDN
|
||||
# LOCALIZATION NOTE (mdnCssProp) String used to describe the 'property name'
|
||||
# parameter used in the 'mdn css' command.
|
||||
mdnCssProp=Property name
|
||||
# LOCALIZATION NOTE (mdnCssPropertyNotFound) String used to display an error in
|
||||
# the result of the 'mdn css' command. Errors occur when a given CSS property
|
||||
# wasn't found on MDN. The %1$S parameter will be replaced with the name of the
|
||||
# CSS property.
|
||||
mdnCssPropertyNotFound=MDN documentation for the CSS property '%1$S' was not found.
|
||||
# LOCALIZATION NOTE (mdnCssVisitPage) String used as the label of a link to the
|
||||
# MDN page for a given CSS property.
|
||||
mdnCssVisitPage=Visit MDN page
|
||||
|
||||
# LOCALIZATION NOTE (security)
|
||||
securityPrivacyDesc=Display supported security and privacy features
|
||||
securityManual=Commands to list and get suggestions about security features for the current domain.
|
||||
|
@ -21,6 +21,11 @@ function doConsoleCalls(aState)
|
||||
let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a");
|
||||
|
||||
top.console.log("foobarBaz-log", undefined);
|
||||
|
||||
top.console.log("Float from not a number: %f", "foo");
|
||||
top.console.log("Float from string: %f", "1.2");
|
||||
top.console.log("Float from number: %f", 1.3);
|
||||
|
||||
top.console.info("foobarBaz-info", null);
|
||||
top.console.warn("foobarBaz-warn", top.document.documentElement);
|
||||
top.console.debug(null);
|
||||
@ -52,6 +57,18 @@ function doConsoleCalls(aState)
|
||||
timeStamp: /^\d+$/,
|
||||
arguments: ["foobarBaz-log", { type: "undefined" }],
|
||||
},
|
||||
{
|
||||
level: "log",
|
||||
arguments: ["Float from not a number: NaN"],
|
||||
},
|
||||
{
|
||||
level: "log",
|
||||
arguments: ["Float from string: 1.200000"],
|
||||
},
|
||||
{
|
||||
level: "log",
|
||||
arguments: ["Float from number: 1.300000"],
|
||||
},
|
||||
{
|
||||
level: "info",
|
||||
filename: /test_consoleapi/,
|
||||
|
@ -1595,9 +1595,14 @@ Console::ProcessArguments(JSContext* aCx,
|
||||
return false;
|
||||
}
|
||||
|
||||
nsCString format;
|
||||
MakeFormatString(format, integer, mantissa, 'f');
|
||||
output.AppendPrintf(format.get(), v);
|
||||
// nspr returns "nan", but we want to expose it as "NaN"
|
||||
if (std::isnan(v)) {
|
||||
output.AppendFloat(v);
|
||||
} else {
|
||||
nsCString format;
|
||||
MakeFormatString(format, integer, mantissa, 'f');
|
||||
output.AppendPrintf(format.get(), v);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -469,6 +469,9 @@
|
||||
android:name="org.mozilla.gecko.dlc.DownloadContentService">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
|
||||
android:exported="false"/>
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import android.Manifest;
|
||||
import android.os.AsyncTask;
|
||||
import org.mozilla.gecko.adjust.AdjustHelperInterface;
|
||||
import org.mozilla.gecko.annotation.RobocopTarget;
|
||||
@ -46,6 +47,7 @@ import org.mozilla.gecko.menu.GeckoMenuItem;
|
||||
import org.mozilla.gecko.mozglue.ContextUtils;
|
||||
import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
|
||||
import org.mozilla.gecko.overlays.ui.ShareDialog;
|
||||
import org.mozilla.gecko.permissions.Permissions;
|
||||
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.prompts.Prompt;
|
||||
@ -60,11 +62,14 @@ import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
|
||||
import org.mozilla.gecko.tabs.TabHistoryFragment;
|
||||
import org.mozilla.gecko.tabs.TabHistoryPage;
|
||||
import org.mozilla.gecko.tabs.TabsPanel;
|
||||
import org.mozilla.gecko.telemetry.TelemetryConstants;
|
||||
import org.mozilla.gecko.telemetry.TelemetryUploadService;
|
||||
import org.mozilla.gecko.toolbar.AutocompleteHandler;
|
||||
import org.mozilla.gecko.toolbar.BrowserToolbar;
|
||||
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
|
||||
import org.mozilla.gecko.toolbar.ToolbarProgressView;
|
||||
import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
|
||||
import org.mozilla.gecko.updater.UpdateServiceHelper;
|
||||
import org.mozilla.gecko.util.ActivityUtils;
|
||||
import org.mozilla.gecko.util.Clipboard;
|
||||
import org.mozilla.gecko.util.EventCallback;
|
||||
@ -156,6 +161,7 @@ import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
public class BrowserApp extends GeckoApp
|
||||
@ -754,6 +760,39 @@ public class BrowserApp extends GeckoApp
|
||||
|
||||
// Set the maximum bits-per-pixel the favicon system cares about.
|
||||
IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
|
||||
|
||||
// The update service is enabled for RELEASE_BUILD, which includes the release and beta channels.
|
||||
// However, no updates are served. Therefore, we don't trust the update service directly, and
|
||||
// try to avoid prompting unnecessarily. See Bug 1232798.
|
||||
if (!AppConstants.RELEASE_BUILD && UpdateServiceHelper.isUpdaterEnabled()) {
|
||||
Permissions.from(this)
|
||||
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.doNotPrompt()
|
||||
.andFallback(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
showUpdaterPermissionSnackbar();
|
||||
}
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void showUpdaterPermissionSnackbar() {
|
||||
SnackbarHelper.SnackbarCallback allowCallback = new SnackbarHelper.SnackbarCallback() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Permissions.from(BrowserApp.this)
|
||||
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.run();
|
||||
}
|
||||
};
|
||||
|
||||
SnackbarHelper.showSnackbarWithAction(this,
|
||||
getString(R.string.updater_permission_text),
|
||||
Snackbar.LENGTH_INDEFINITE,
|
||||
getString(R.string.updater_permission_allow),
|
||||
allowCallback);
|
||||
}
|
||||
|
||||
private void conditionallyNotifyHCEOL() {
|
||||
@ -958,7 +997,8 @@ public class BrowserApp extends GeckoApp
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getProfile().inGuestMode()) {
|
||||
final GeckoProfile profile = getProfile();
|
||||
if (profile.inGuestMode()) {
|
||||
GuestSession.showNotification(BrowserApp.this);
|
||||
} else {
|
||||
// If we're restarting, we won't destroy the activity.
|
||||
@ -966,6 +1006,15 @@ public class BrowserApp extends GeckoApp
|
||||
// have been shown.
|
||||
GuestSession.hideNotification(BrowserApp.this);
|
||||
}
|
||||
|
||||
// We don't upload in onCreate because that's only called when the Activity needs to be instantiated
|
||||
// and it's possible the system will never free the Activity from memory.
|
||||
//
|
||||
// We don't upload in onResume/onPause because that will be called each time the Activity is obscured,
|
||||
// including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured.
|
||||
//
|
||||
// So we're left with onStart/onStop.
|
||||
uploadTelemetry(profile);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -3883,6 +3932,26 @@ public class BrowserApp extends GeckoApp
|
||||
mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE);
|
||||
}
|
||||
|
||||
private void uploadTelemetry(final GeckoProfile profile) {
|
||||
if (!TelemetryConstants.UPLOAD_ENABLED || profile.inGuestMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(this, profile.getName());
|
||||
final int seq = sharedPrefs.getInt(TelemetryConstants.PREF_SEQ_COUNT, 1);
|
||||
|
||||
final Intent i = new Intent(TelemetryConstants.ACTION_UPLOAD_CORE);
|
||||
i.setClass(this, TelemetryUploadService.class);
|
||||
i.putExtra(TelemetryConstants.EXTRA_DOC_ID, UUID.randomUUID().toString());
|
||||
i.putExtra(TelemetryConstants.EXTRA_PROFILE_NAME, profile.getName());
|
||||
i.putExtra(TelemetryConstants.EXTRA_PROFILE_PATH, profile.getDir().toString());
|
||||
i.putExtra(TelemetryConstants.EXTRA_SEQ, seq);
|
||||
startService(i);
|
||||
|
||||
// Intent redelivery will ensure this value gets used - see TelemetryUploadService class comments for details.
|
||||
sharedPrefs.edit().putInt(TelemetryConstants.PREF_SEQ_COUNT, seq + 1).apply();
|
||||
}
|
||||
|
||||
public static interface Refreshable {
|
||||
public void refresh();
|
||||
}
|
||||
|
@ -38,12 +38,18 @@ import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public final class GeckoProfile {
|
||||
private static final String LOGTAG = "GeckoProfile";
|
||||
|
||||
// The path in the profile to the file containing the client ID.
|
||||
private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
|
||||
// In the client ID file, the attribute title in the JSON object containing the client ID value.
|
||||
private static final String CLIENT_ID_JSON_ATTR = "clientID";
|
||||
|
||||
// Only tests should need to do this.
|
||||
// We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
|
||||
private static volatile boolean sAcceptDirectoryChanges = true;
|
||||
@ -588,6 +594,39 @@ public final class GeckoProfile {
|
||||
return new File(f, aFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Gecko client ID from the filesystem.
|
||||
*
|
||||
* This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
|
||||
* this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
|
||||
*
|
||||
* WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
|
||||
* robust way to access it. However, we don't want to rely on Gecko running in order to get
|
||||
* the client ID so instead we access the file this module accesses directly. However, it's
|
||||
* possible the format of this file (and the access calls in the jsm) will change, leaving
|
||||
* this code to fail.
|
||||
*
|
||||
* TODO: Write tests to prevent regressions. Mention them here. Test both file location and file format.
|
||||
*
|
||||
* [1]: https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
|
||||
*/
|
||||
@WorkerThread
|
||||
public String getClientId() throws IOException {
|
||||
final String clientIdFileContents;
|
||||
try {
|
||||
clientIdFileContents = readFile(CLIENT_ID_FILE_PATH);
|
||||
} catch (final IOException e) {
|
||||
throw new IOException("Could not read client ID file to retrieve client ID", e);
|
||||
}
|
||||
|
||||
try {
|
||||
final org.json.JSONObject json = new org.json.JSONObject(clientIdFileContents);
|
||||
return json.getString(CLIENT_ID_JSON_ATTR);
|
||||
} catch (final JSONException e) {
|
||||
throw new IOException("Could not parse JSON to retrieve client ID", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the session file to the backup session file.
|
||||
*
|
||||
|
@ -1,4 +1,7 @@
|
||||
|
||||
/* -*- 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.gecko.dlc;
|
||||
|
||||
|
@ -65,11 +65,18 @@ public class PermissionBlock {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute this permission block. Calling this method will prompt the user if needed.
|
||||
*/
|
||||
public void run() {
|
||||
run(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
|
||||
* user if needed.
|
||||
*/
|
||||
public void run(@NonNull Runnable onPermissionsGranted) {
|
||||
public void run(Runnable onPermissionsGranted) {
|
||||
if (!doNotPrompt && !(context instanceof Activity)) {
|
||||
throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context");
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
/* 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.telemetry;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
|
||||
public class TelemetryConstants {
|
||||
|
||||
// Change these two values to enable upload in developer builds.
|
||||
public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds.
|
||||
public static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org";
|
||||
|
||||
public static final String USER_AGENT =
|
||||
"Firefox-Android-Telemetry/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
|
||||
|
||||
public static final String ACTION_UPLOAD_CORE = "uploadCore";
|
||||
public static final String EXTRA_DOC_ID = "docId";
|
||||
public static final String EXTRA_PROFILE_NAME = "geckoProfileName";
|
||||
public static final String EXTRA_PROFILE_PATH = "geckoProfilePath";
|
||||
public static final String EXTRA_SEQ = "seq";
|
||||
|
||||
public static final String PREF_SERVER_URL = "telemetry-serverUrl";
|
||||
public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
|
||||
|
||||
public static class CorePing {
|
||||
private CorePing() { /* To prevent instantiation */ }
|
||||
|
||||
public static final String NAME = "core";
|
||||
public static final int VERSION_VALUE = 1;
|
||||
public static final String OS_VALUE = "Android";
|
||||
|
||||
public static final String ARCHITECTURE = "arch";
|
||||
public static final String CLIENT_ID = "clientId";
|
||||
public static final String DEVICE = "device";
|
||||
public static final String EXPERIMENTS = "experiments";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String OS_ATTR = "os";
|
||||
public static final String OS_VERSION = "osversion";
|
||||
public static final String SEQ = "seq";
|
||||
public static final String VERSION_ATTR = "v";
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.telemetry;
|
||||
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
|
||||
/**
|
||||
* Container for telemetry data and the data necessary to upload it.
|
||||
*/
|
||||
public class TelemetryPing {
|
||||
private final String url;
|
||||
private final ExtendedJSONObject payload;
|
||||
|
||||
public TelemetryPing(final String url, final ExtendedJSONObject payload) {
|
||||
this.url = url;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public String getURL() { return url; }
|
||||
public ExtendedJSONObject getPayload() { return payload; }
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.telemetry;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.keepsafe.switchboard.SwitchBoard;
|
||||
import org.json.JSONArray;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.Locales;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
|
||||
*/
|
||||
public class TelemetryPingGenerator {
|
||||
|
||||
// In the server url, the initial path directly after the "scheme://host:port/"
|
||||
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
|
||||
|
||||
/**
|
||||
* Returns a url of the format:
|
||||
* http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
|
||||
*
|
||||
* @param docId A unique document ID for the ping associated with the upload to this server
|
||||
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
|
||||
* @param docType The name of the ping (e.g. "main")
|
||||
* @return a url at which to POST the telemetry data to
|
||||
*/
|
||||
private static String getTelemetryServerURL(final String docId, final String serverURLSchemeHostPort,
|
||||
final String docType) {
|
||||
final String appName = AppConstants.MOZ_APP_BASENAME;
|
||||
final String appVersion = AppConstants.MOZ_APP_VERSION;
|
||||
final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
|
||||
final String appBuildId = AppConstants.MOZ_APP_BUILDID;
|
||||
|
||||
// The compiler will optimize a single String concatenation into a StringBuilder statement.
|
||||
// If you change this `return`, be sure to keep it as a single statement to keep it optimized!
|
||||
return serverURLSchemeHostPort + '/' +
|
||||
SERVER_INITIAL_PATH + '/' +
|
||||
docId + '/' +
|
||||
docType + '/' +
|
||||
appName + '/' +
|
||||
appVersion + '/' +
|
||||
appUpdateChannel + '/' +
|
||||
appBuildId + '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param docId A unique document ID for the ping associated with the upload to this server
|
||||
* @param clientId The client ID of this profile (from Gecko)
|
||||
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
|
||||
* @throws IOException when client ID could not be created
|
||||
*/
|
||||
public static TelemetryPing createCorePing(final Context context, final String docId, final String clientId,
|
||||
final String serverURLSchemeHostPort, final int seq) {
|
||||
final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
|
||||
final ExtendedJSONObject payload = createCorePingPayload(context, clientId, seq);
|
||||
return new TelemetryPing(serverURL, payload);
|
||||
}
|
||||
|
||||
private static ExtendedJSONObject createCorePingPayload(final Context context, final String clientId,
|
||||
final int seq) {
|
||||
final ExtendedJSONObject ping = new ExtendedJSONObject();
|
||||
ping.put(CorePing.VERSION_ATTR, CorePing.VERSION_VALUE);
|
||||
ping.put(CorePing.OS_ATTR, CorePing.OS_VALUE);
|
||||
|
||||
// We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
|
||||
// manufacturer because we're less likely to have manufacturers with similar names than we are for a
|
||||
// manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
|
||||
final String deviceDescriptor =
|
||||
StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
|
||||
|
||||
ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
|
||||
ping.put(CorePing.CLIENT_ID, clientId);
|
||||
ping.put(CorePing.DEVICE, deviceDescriptor);
|
||||
ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
|
||||
ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
|
||||
ping.put(CorePing.SEQ, seq);
|
||||
if (AppConstants.MOZ_SWITCHBOARD) {
|
||||
ping.put(CorePing.EXPERIMENTS, getActiveExperiments(context));
|
||||
}
|
||||
return ping;
|
||||
}
|
||||
|
||||
private static JSONArray getActiveExperiments(final Context context) {
|
||||
if (!AppConstants.MOZ_SWITCHBOARD) {
|
||||
throw new IllegalStateException("This method should not be called with switchboard disabled");
|
||||
}
|
||||
return new JSONArray(SwitchBoard.getActiveExperiments(context));
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
/* 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.telemetry;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.background.BackgroundService;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
|
||||
import org.mozilla.gecko.sync.net.Resource;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
/**
|
||||
* The service that handles uploading telemetry payloads to the server.
|
||||
*
|
||||
* Note that we'll fail to upload if the network is off or background uploads are disabled but the caller is still
|
||||
* expected to increment the sequence number.
|
||||
*/
|
||||
public class TelemetryUploadService extends BackgroundService {
|
||||
private static final String LOGTAG = StringUtils.safeSubstring("Gecko" + TelemetryUploadService.class.getSimpleName(), 0, 23);
|
||||
private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
|
||||
|
||||
public TelemetryUploadService() {
|
||||
super(WORKER_THREAD_NAME);
|
||||
|
||||
// Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat) so for
|
||||
// simplicity, we avoid it for now. In the unlikely event that Android kills our upload service, we'll thus fail
|
||||
// to upload the document with a specific sequence number. Furthermore, we never attempt to re-upload it.
|
||||
//
|
||||
// We'll fix this issue in bug 1243585.
|
||||
setIntentRedelivery(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a core ping with the mandatory extras:
|
||||
* EXTRA_DOC_ID: a unique document ID.
|
||||
* EXTRA_SEQ: a sequence number for this upload.
|
||||
* EXTRA_PROFILE_NAME: the gecko profile name.
|
||||
* EXTRA_PROFILE_PATH: the gecko profile path.
|
||||
*
|
||||
* Note that for a given doc ID, seq should always be identical because these are the tools the server uses to
|
||||
* de-duplicate documents. In order to maintain this consistency, we receive the doc ID and seq from the Intent and
|
||||
* rely on the caller to update the values. The Service can be killed at any time so we can't ensure seq could be
|
||||
* incremented properly if we tried to do so in the Service.
|
||||
*/
|
||||
@Override
|
||||
public void onHandleIntent(final Intent intent) {
|
||||
Log.d(LOGTAG, "Service started");
|
||||
|
||||
if (!TelemetryConstants.UPLOAD_ENABLED) {
|
||||
Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled; not handling upload intent.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReadyToUpload(intent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TelemetryConstants.ACTION_UPLOAD_CORE.equals(intent.getAction())) {
|
||||
Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning");
|
||||
return;
|
||||
}
|
||||
|
||||
final String docId = intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID);
|
||||
final int seq = intent.getIntExtra(TelemetryConstants.EXTRA_SEQ, -1);
|
||||
|
||||
final String profileName = intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME);
|
||||
final String profilePath = intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH);
|
||||
|
||||
uploadCorePing(docId, seq, profileName, profilePath);
|
||||
}
|
||||
|
||||
private boolean isReadyToUpload(final Intent intent) {
|
||||
// Intent can be null. Bug 1025937.
|
||||
if (intent == null) {
|
||||
Log.d(LOGTAG, "Received null intent. Returning.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't do anything if the device can't talk to the server.
|
||||
if (!backgroundDataIsEnabled()) {
|
||||
Log.d(LOGTAG, "Background data is not enabled; skipping.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID) == null) {
|
||||
Log.w(LOGTAG, "Received invalid doc ID in Intent. Returning");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!intent.hasExtra(TelemetryConstants.EXTRA_SEQ)) {
|
||||
Log.w(LOGTAG, "Received Intent without sequence number. Returning");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME) == null) {
|
||||
Log.w(LOGTAG, "Received invalid profile name in Intent. Returning");
|
||||
return false;
|
||||
}
|
||||
|
||||
// GeckoProfile can use the name to get the path so this isn't strictly necessary.
|
||||
// However, getting the path requires parsing an ini file so we optimize by including it here.
|
||||
if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH) == null) {
|
||||
Log.w(LOGTAG, "Received invalid profile path in Intent. Returning");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void uploadCorePing(@NonNull final String docId, final int seq, @NonNull final String profileName,
|
||||
@NonNull final String profilePath) {
|
||||
final GeckoProfile profile = GeckoProfile.get(this, profileName, profilePath);
|
||||
|
||||
final String clientId;
|
||||
try {
|
||||
clientId = profile.getClientId();
|
||||
} catch (final IOException e) {
|
||||
// Don't log the exception to avoid leaking the profile path.
|
||||
Log.w(LOGTAG, "Unable to get client ID to generate core ping: returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
|
||||
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(this, profileName);
|
||||
// TODO (bug 1241685): Sync this preference with the gecko preference.
|
||||
final String serverURLSchemeHostPort =
|
||||
sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
|
||||
|
||||
final TelemetryPing corePing =
|
||||
TelemetryPingGenerator.createCorePing(this, docId, clientId, serverURLSchemeHostPort, seq);
|
||||
final CorePingResultDelegate resultDelegate = new CorePingResultDelegate();
|
||||
uploadPing(corePing, resultDelegate);
|
||||
}
|
||||
|
||||
private void uploadPing(final TelemetryPing ping, final ResultDelegate delegate) {
|
||||
final BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(ping.getURL());
|
||||
} catch (final URISyntaxException e) {
|
||||
Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
delegate.setResource(resource);
|
||||
resource.delegate = delegate;
|
||||
|
||||
// We're in a background thread so we don't have any reason to do this asynchronously.
|
||||
// If we tried, onStartCommand would return and IntentService might stop itself before we finish.
|
||||
resource.postBlocking(ping.getPayload());
|
||||
}
|
||||
|
||||
private static class CorePingResultDelegate extends ResultDelegate {
|
||||
public CorePingResultDelegate() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserAgent() {
|
||||
return TelemetryConstants.USER_AGENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(final HttpResponse response) {
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
switch (status) {
|
||||
case 200:
|
||||
case 201:
|
||||
Log.d(LOGTAG, "Telemetry upload success.");
|
||||
break;
|
||||
default:
|
||||
Log.w(LOGTAG, "Telemetry upload failure. HTTP status: " + status);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(final ClientProtocolException e) {
|
||||
// We don't log the exception to prevent leaking user data.
|
||||
Log.w(LOGTAG, "HttpProtocolException when trying to upload telemetry");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(final IOException e) {
|
||||
// We don't log the exception to prevent leaking user data.
|
||||
Log.w(LOGTAG, "HttpIOException when trying to upload telemetry");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(final GeneralSecurityException e) {
|
||||
// We don't log the exception to prevent leaking user data.
|
||||
Log.w(LOGTAG, "Transport exception when trying to upload telemetry");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hack because I want to set the resource after the Delegate is constructed.
|
||||
* Be sure to call {@link #setResource(Resource)}!
|
||||
*/
|
||||
private static abstract class ResultDelegate extends BaseResourceDelegate {
|
||||
public ResultDelegate() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
protected void setResource(final Resource resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
package org.mozilla.gecko.util;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
@ -240,4 +241,10 @@ public class StringUtils {
|
||||
|
||||
return Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
public static String safeSubstring(@NonNull final String str, final int start, final int end) {
|
||||
return str.substring(
|
||||
Math.max(0, start),
|
||||
Math.min(end, str.length()));
|
||||
}
|
||||
}
|
||||
|
@ -540,6 +540,10 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'tabs/TabsPanel.java',
|
||||
'tabs/TabsPanelThumbnailView.java',
|
||||
'Telemetry.java',
|
||||
'telemetry/TelemetryConstants.java',
|
||||
'telemetry/TelemetryPing.java',
|
||||
'telemetry/TelemetryPingGenerator.java',
|
||||
'telemetry/TelemetryUploadService.java',
|
||||
'TelemetryContract.java',
|
||||
'TextSelection.java',
|
||||
'TextSelectionHandle.java',
|
||||
|
@ -541,6 +541,7 @@
|
||||
|
||||
<string name="updater_permission_title">&brandShortName;</string>
|
||||
<string name="updater_permission_text">&updater_permission_text;</string>
|
||||
<string name="updater_permission_allow">&updater_permission_allow;</string>
|
||||
|
||||
<!-- Awesomescreen screen -->
|
||||
<string name="suggestions_prompt">&suggestions_prompt3;</string>
|
||||
|
@ -497,6 +497,16 @@ public class BaseResource implements Resource {
|
||||
post(jsonEntity(o));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an HTTP POST as with {@link BaseResource#post(ExtendedJSONObject)}, returning only
|
||||
* after callbacks have been invoked.
|
||||
*/
|
||||
public void postBlocking(final ExtendedJSONObject o) {
|
||||
// Until we use the asynchronous Apache HttpClient, we can simply call
|
||||
// through.
|
||||
post(jsonEntity(o));
|
||||
}
|
||||
|
||||
public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
|
||||
post(jsonEntity(jsonObject));
|
||||
}
|
||||
|
@ -545,6 +545,8 @@ Statement::Reset()
|
||||
NS_IMETHODIMP
|
||||
Statement::BindParameters(mozIStorageBindingParamsArray *aParameters)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aParameters);
|
||||
|
||||
if (!mDBStatement)
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
|
||||
|
@ -165,6 +165,16 @@ function test_failed_execute()
|
||||
stmt.finalize();
|
||||
}
|
||||
|
||||
function test_bind_undefined()
|
||||
{
|
||||
var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
|
||||
|
||||
expectError(Cr.NS_ERROR_ILLEGAL_VALUE,
|
||||
() => stmt.bindParameters(undefined));
|
||||
|
||||
stmt.finalize();
|
||||
}
|
||||
|
||||
var tests = [test_parameterCount_none, test_parameterCount_one,
|
||||
test_getParameterName, test_getParameterIndex_different,
|
||||
test_getParameterIndex_same, test_columnCount,
|
||||
@ -173,6 +183,7 @@ var tests = [test_parameterCount_none, test_parameterCount_one,
|
||||
test_state_executing, test_state_after_finalize,
|
||||
test_getColumnDecltype,
|
||||
test_failed_execute,
|
||||
test_bind_undefined,
|
||||
];
|
||||
|
||||
function run_test()
|
||||
|
@ -50,6 +50,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
||||
|
||||
@ -203,6 +205,8 @@ var Management = {
|
||||
// extension pages (which run in the chrome process).
|
||||
var globalBroker = new MessageBroker([Services.mm, Services.ppmm]);
|
||||
|
||||
var gContextId = 0;
|
||||
|
||||
// An extension page is an execution context for any extension content
|
||||
// that runs in the chrome process. It's used for background pages
|
||||
// (type="background"), popups (type="popup"), and any extension
|
||||
@ -222,6 +226,8 @@ ExtensionPage = function(extension, params) {
|
||||
this.uri = uri || extension.baseURI;
|
||||
this.incognito = params.incognito || false;
|
||||
this.onClose = new Set();
|
||||
this.contextId = gContextId++;
|
||||
this.unloaded = false;
|
||||
|
||||
// This is the MessageSender property passed to extension.
|
||||
// It can be augmented by the "page-open" hook.
|
||||
@ -270,6 +276,17 @@ ExtensionPage.prototype = {
|
||||
return true;
|
||||
},
|
||||
|
||||
// A wrapper around MessageChannel.sendMessage which adds the extension ID
|
||||
// to the recipient object, and ensures replies are not processed after the
|
||||
// context has been unloaded.
|
||||
sendMessage(target, messageName, data, recipient = {}, sender = {}) {
|
||||
recipient.extensionId = this.extension.id;
|
||||
sender.extensionId = this.extension.id;
|
||||
sender.contextId = this.contextId;
|
||||
|
||||
return MessageChannel.sendMessage(target, messageName, data, recipient, sender);
|
||||
},
|
||||
|
||||
callOnClose(obj) {
|
||||
this.onClose.add(obj);
|
||||
},
|
||||
@ -287,6 +304,20 @@ ExtensionPage.prototype = {
|
||||
// This method is called when an extension page navigates away or
|
||||
// its tab is closed.
|
||||
unload() {
|
||||
// Note that without this guard, we end up running unload code
|
||||
// multiple times for tab pages closed by the "page-unload" handlers
|
||||
// triggered below.
|
||||
if (this.unloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unloaded = true;
|
||||
|
||||
MessageChannel.abortResponses({
|
||||
extensionId: this.extension.id,
|
||||
contextId: this.contextId,
|
||||
});
|
||||
|
||||
Management.emit("page-unload", this);
|
||||
|
||||
this.extension.views.delete(this);
|
||||
@ -1076,6 +1107,8 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
|
||||
|
||||
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
|
||||
|
||||
MessageChannel.abortResponses({ extensionId: this.id });
|
||||
|
||||
ExtensionManagement.shutdownExtension(this.uuid);
|
||||
|
||||
// Clean up a generated file.
|
||||
|
@ -29,6 +29,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
||||
"resource://gre/modules/MatchPattern.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||
"resource://gre/modules/MessageChannel.jsm");
|
||||
|
||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||
var {
|
||||
@ -107,12 +111,14 @@ var api = context => {
|
||||
};
|
||||
|
||||
// Represents a content script.
|
||||
function Script(options) {
|
||||
function Script(options, deferred = PromiseUtils.defer()) {
|
||||
this.options = options;
|
||||
this.run_at = this.options.run_at;
|
||||
this.js = this.options.js || [];
|
||||
this.css = this.options.css || [];
|
||||
|
||||
this.deferred = deferred;
|
||||
|
||||
this.matches_ = new MatchPattern(this.options.matches);
|
||||
this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
|
||||
// TODO: MatchPattern should pre-mangle host-only patterns so that we
|
||||
@ -137,16 +143,6 @@ Script.prototype = {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("innerWindowID" in this.options) {
|
||||
let innerWindowID = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.currentInnerWindowID;
|
||||
|
||||
if (innerWindowID !== this.options.innerWindowID) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: match_about_blank.
|
||||
|
||||
return true;
|
||||
@ -154,6 +150,7 @@ Script.prototype = {
|
||||
|
||||
tryInject(extension, window, sandbox, shouldRun) {
|
||||
if (!this.matches(window)) {
|
||||
this.deferred.reject();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -172,6 +169,7 @@ Script.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
let result;
|
||||
let scheduled = this.run_at || "document_idle";
|
||||
if (shouldRun(scheduled)) {
|
||||
for (let url of this.js) {
|
||||
@ -189,13 +187,26 @@ Script.prototype = {
|
||||
charset: "UTF-8",
|
||||
async: AppConstants.platform == "gonk",
|
||||
};
|
||||
runSafeSyncWithoutClone(Services.scriptloader.loadSubScriptWithOptions, url, options);
|
||||
try {
|
||||
result = Services.scriptloader.loadSubScriptWithOptions(url, options);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
this.deferred.reject(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.jsCode) {
|
||||
Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
|
||||
try {
|
||||
result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
this.deferred.reject(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle this correctly when we support runAt and allFrames.
|
||||
this.deferred.resolve(result);
|
||||
},
|
||||
};
|
||||
|
||||
@ -404,6 +415,8 @@ var DocumentManager = {
|
||||
} else if (topic == "inner-window-destroyed") {
|
||||
let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
|
||||
MessageChannel.abortResponses({ innerWindowID: windowId });
|
||||
|
||||
// Close any existent content-script context for the destroyed window.
|
||||
if (this.contentScriptWindows.has(windowId)) {
|
||||
let extensions = this.contentScriptWindows.get(windowId);
|
||||
@ -446,7 +459,7 @@ var DocumentManager = {
|
||||
let window = global.content;
|
||||
let context = this.getContentScriptContext(extensionId, window);
|
||||
if (!context) {
|
||||
return;
|
||||
throw new Error("Unexpected add-on ID");
|
||||
}
|
||||
|
||||
// TODO: Somehow make sure we have the right permissions for this origin!
|
||||
@ -535,6 +548,8 @@ var DocumentManager = {
|
||||
}
|
||||
}
|
||||
|
||||
MessageChannel.abortResponses({ extensionId });
|
||||
|
||||
this.extensionCount--;
|
||||
if (this.extensionCount == 0) {
|
||||
this.uninit();
|
||||
@ -645,44 +660,63 @@ ExtensionManager = {
|
||||
},
|
||||
};
|
||||
|
||||
class ExtensionGlobal {
|
||||
constructor(global) {
|
||||
this.global = global;
|
||||
|
||||
MessageChannel.addListener(global, "Extension:Execute", this);
|
||||
|
||||
this.broker = new MessageBroker([global]);
|
||||
|
||||
this.windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
|
||||
global.sendAsyncMessage("Extension:TopWindowID", { windowId: this.windowId });
|
||||
}
|
||||
|
||||
uninit() {
|
||||
this.global.sendAsyncMessage("Extension:RemoveTopWindowID", { windowId: this.windowId });
|
||||
}
|
||||
|
||||
get messageFilter() {
|
||||
return {
|
||||
innerWindowID: this.global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.currentInnerWindowID,
|
||||
};
|
||||
}
|
||||
|
||||
receiveMessage({ target, messageName, recipient, data }) {
|
||||
switch (messageName) {
|
||||
case "Extension:Execute":
|
||||
let deferred = PromiseUtils.defer();
|
||||
|
||||
let script = new Script(data.options, deferred);
|
||||
let { extensionId } = recipient;
|
||||
DocumentManager.executeScript(target, extensionId, script);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ExtensionContent = {
|
||||
globals: new Map(),
|
||||
|
||||
init(global) {
|
||||
let broker = new MessageBroker([global]);
|
||||
this.globals.set(global, broker);
|
||||
|
||||
global.addMessageListener("Extension:Execute", this);
|
||||
|
||||
let windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
global.sendAsyncMessage("Extension:TopWindowID", {windowId});
|
||||
this.globals.set(global, new ExtensionGlobal(global));
|
||||
},
|
||||
|
||||
uninit(global) {
|
||||
this.globals.get(global).uninit();
|
||||
this.globals.delete(global);
|
||||
|
||||
let windowId = global.content
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils)
|
||||
.outerWindowID;
|
||||
global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId});
|
||||
},
|
||||
|
||||
getBroker(messageManager) {
|
||||
return this.globals.get(messageManager);
|
||||
},
|
||||
|
||||
receiveMessage({target, name, data}) {
|
||||
switch (name) {
|
||||
case "Extension:Execute":
|
||||
let script = new Script(data.options);
|
||||
let {extensionId} = data;
|
||||
DocumentManager.executeScript(target, extensionId, script);
|
||||
break;
|
||||
}
|
||||
return this.globals.get(messageManager).broker;
|
||||
},
|
||||
};
|
||||
|
||||
|
610
toolkit/components/extensions/MessageChannel.jsm
Normal file
@ -0,0 +1,610 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This module provides wrappers around standard message managers to
|
||||
* simplify bidirectional communication. It currently allows a caller to
|
||||
* send a message to a single listener, and receive a reply. If there
|
||||
* are no matching listeners, or the message manager disconnects before
|
||||
* a reply is received, the caller is returned an error.
|
||||
*
|
||||
* Since each message must have only one recipient, the listener end may
|
||||
* specify filters for the messages it wishes to receive, and the sender
|
||||
* end likewise may specify recipient tags to match the filters.
|
||||
*
|
||||
* The message handler on the listener side may return its response
|
||||
* value directly, or may return a promise, the resolution or rejection
|
||||
* of which will be returned instead. The sender end likewise receives a
|
||||
* promise which resolves or rejects to the listener's response.
|
||||
*
|
||||
*
|
||||
* A basic setup works something like this:
|
||||
*
|
||||
* A content script adds a message listener to its global
|
||||
* nsIContentFrameMessageManager, with an appropriate set of filters:
|
||||
*
|
||||
* {
|
||||
* init(messageManager, window, extensionID) {
|
||||
* this.window = window;
|
||||
*
|
||||
* MessageChannel.addListener(
|
||||
* messageManager, "ContentScript:TouchContent",
|
||||
* this);
|
||||
*
|
||||
* this.messageFilter = {
|
||||
* innerWindowID: getInnerWindowID(window),
|
||||
* extensionID: extensionID,
|
||||
* };
|
||||
* },
|
||||
*
|
||||
* receiveMessage({ target, messageName, sender, recipient, data }) {
|
||||
* if (messageName == "ContentScript:TouchContent") {
|
||||
* return new Promise(resolve => {
|
||||
* this.touchWindow(data.touchWith, result => {
|
||||
* resolve({ touchResult: result });
|
||||
* });
|
||||
* });
|
||||
* }
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* A script in the parent process sends a message to the content process
|
||||
* via a tab message manager, including recipient tags to match its
|
||||
* filter, and an optional sender tag to identify itself:
|
||||
*
|
||||
* let data = { touchWith: "pencil" };
|
||||
* let sender = { extensionID, contextID };
|
||||
* let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
|
||||
*
|
||||
* MessageChannel.sendMessage(
|
||||
* tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
|
||||
* data, recipient, sender
|
||||
* ).then(result => {
|
||||
* alert(result.touchResult);
|
||||
* });
|
||||
*
|
||||
* Since the lifetimes of message senders and receivers may not always
|
||||
* match, either side of the message channel may cancel pending
|
||||
* responses which match its sender or recipient tags.
|
||||
*
|
||||
* For the above client, this might be done from an
|
||||
* inner-window-destroyed observer, when its target scope is destroyed:
|
||||
*
|
||||
* observe(subject, topic, data) {
|
||||
* if (topic == "inner-window-destroyed") {
|
||||
* let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
||||
*
|
||||
* MessageChannel.abortResponses({ innerWindowID });
|
||||
* }
|
||||
* },
|
||||
*
|
||||
* From the parent, it may be done when its context is being destroyed:
|
||||
*
|
||||
* onDestroy() {
|
||||
* MessageChannel.abortResponses({
|
||||
* extensionID: this.extensionID,
|
||||
* contextID: this.contextID,
|
||||
* });
|
||||
* },
|
||||
*
|
||||
*/
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MessageChannel"];
|
||||
|
||||
/* globals MessageChannel */
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||
"resource://gre/modules/PromiseUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
|
||||
/**
|
||||
* Handles the mapping and dispatching of messages to their registered
|
||||
* handlers. There is one broker per message manager and class of
|
||||
* messages. Each class of messages is mapped to one native message
|
||||
* name, e.g., "MessageChannel:Message", and is dispatched to handlers
|
||||
* based on an internal message name, e.g., "Extension:ExecuteScript".
|
||||
*/
|
||||
class FilteringMessageManager {
|
||||
/**
|
||||
* @param {string} messageName
|
||||
* The name of the native message this broker listens for.
|
||||
* @param {function} callback
|
||||
* A function which is called for each message after it has been
|
||||
* mapped to its handler. The function receives two arguments:
|
||||
*
|
||||
* result:
|
||||
* An object containing either a `handler` or an `error` property.
|
||||
* If no error occurs, `handler` will be a matching handler that
|
||||
* was registered by `addHandler`. Otherwise, the `error` property
|
||||
* will contain an object describing the error.
|
||||
*
|
||||
* data:
|
||||
* An object describing the message, as defined in
|
||||
* `MessageChannel.addListener`.
|
||||
*/
|
||||
constructor(messageName, callback, messageManager) {
|
||||
this.messageName = messageName;
|
||||
this.callback = callback;
|
||||
this.messageManager = messageManager;
|
||||
|
||||
this.messageManager.addMessageListener(this.messageName, this);
|
||||
|
||||
this.handlers = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a message from our message manager, maps it to a handler, and
|
||||
* passes the result to our message callback.
|
||||
*/
|
||||
receiveMessage({ data, target }) {
|
||||
let handlers = Array.from(this.getHandlers(data.messageName, data.recipient));
|
||||
|
||||
let result = {};
|
||||
if (handlers.length == 0) {
|
||||
result.error = { result: MessageChannel.RESULT_NO_HANDLER,
|
||||
message: "No matching message handler" };
|
||||
} else if (handlers.length > 1) {
|
||||
result.error = { result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
|
||||
message: `Multiple matching handlers for ${data.messageName}` };
|
||||
} else {
|
||||
result.handler = handlers[0];
|
||||
}
|
||||
|
||||
data.target = target;
|
||||
this.callback(result, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all handlers for the given message name. If `recipient`
|
||||
* is provided, only iterates over handlers whose filters match it.
|
||||
*
|
||||
* @param {string|number} messageName
|
||||
* The message for which to return handlers.
|
||||
* @param {object} recipient
|
||||
* The recipient data on which to filter handlers.
|
||||
*/
|
||||
* getHandlers(messageName, recipient) {
|
||||
let handlers = this.handlers.get(messageName) || new Set();
|
||||
for (let handler of handlers) {
|
||||
if (MessageChannel.matchesFilter(handler.messageFilter, recipient)) {
|
||||
yield handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler for the given message.
|
||||
*
|
||||
* @param {string} messageName
|
||||
* The internal message name for which to register the handler.
|
||||
* @param {object} handler
|
||||
* An opaque handler object. The object must have a `messageFilter`
|
||||
* property on which to filter messages. Final dispatching is handled
|
||||
* by the message callback passed to the constructor.
|
||||
*/
|
||||
addHandler(messageName, handler) {
|
||||
if (!this.handlers.has(messageName)) {
|
||||
this.handlers.set(messageName, new Set());
|
||||
}
|
||||
|
||||
this.handlers.get(messageName).add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a handler for the given message.
|
||||
*
|
||||
* @param {string} messageName
|
||||
* The internal message name for which to unregister the handler.
|
||||
* @param {object} handler
|
||||
* The handler object to unregister.
|
||||
*/
|
||||
removeHandler(messageName, handler) {
|
||||
this.handlers.get(messageName).delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages mappings of message managers to their corresponding message
|
||||
* brokers. Brokers are lazily created for each message manager the
|
||||
* first time they are accessed. In the case of content frame message
|
||||
* managers, they are also automatically destroyed when the frame
|
||||
* unload event fires.
|
||||
*/
|
||||
class FilteringMessageManagerMap extends Map {
|
||||
// Unfortunately, we can't use a WeakMap for this, because message
|
||||
// managers do not support preserved wrappers.
|
||||
|
||||
/**
|
||||
* @param {string} messageName
|
||||
* The native message name passed to `FilteringMessageManager` constructors.
|
||||
* @param {function} callback
|
||||
* The message callback function passed to
|
||||
* `FilteringMessageManager` constructors.
|
||||
*/
|
||||
constructor(messageName, callback) {
|
||||
super();
|
||||
|
||||
this.messageName = messageName;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, and possibly creates, a message broker for the given
|
||||
* message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager for which to return a broker.
|
||||
*
|
||||
* @returns {FilteringMessageManager}
|
||||
*/
|
||||
get(target) {
|
||||
if (this.has(target)) {
|
||||
return super.get(target);
|
||||
}
|
||||
|
||||
let broker = new FilteringMessageManager(this.messageName, this.callback, target);
|
||||
this.set(target, broker);
|
||||
|
||||
if (target instanceof Ci.nsIDOMEventTarget) {
|
||||
let onUnload = event => {
|
||||
target.removeEventListener("unload", onUnload);
|
||||
this.delete(target);
|
||||
};
|
||||
target.addEventListener("unload", onUnload);
|
||||
}
|
||||
|
||||
return broker;
|
||||
}
|
||||
}
|
||||
|
||||
const MESSAGE_MESSAGE = "MessageChannel:Message";
|
||||
const MESSAGE_RESPONSE = "MessageChannel:Response";
|
||||
|
||||
let gChannelId = 0;
|
||||
|
||||
this.MessageChannel = {
|
||||
init() {
|
||||
Services.obs.addObserver(this, "message-manager-close", false);
|
||||
Services.obs.addObserver(this, "message-manager-disconnect", false);
|
||||
|
||||
this.messageManagers = new FilteringMessageManagerMap(
|
||||
MESSAGE_MESSAGE, this._handleMessage.bind(this));
|
||||
|
||||
this.responseManagers = new FilteringMessageManagerMap(
|
||||
MESSAGE_RESPONSE, this._handleResponse.bind(this));
|
||||
|
||||
/**
|
||||
* Contains a list of pending responses, either waiting to be
|
||||
* received or waiting to be sent. @see _addPendingResponse
|
||||
*/
|
||||
this.pendingResponses = new Set();
|
||||
},
|
||||
|
||||
RESULT_SUCCESS: 0,
|
||||
RESULT_DISCONNECTED: 1,
|
||||
RESULT_NO_HANDLER: 2,
|
||||
RESULT_MULTIPLE_HANDLERS: 3,
|
||||
RESULT_ERROR: 4,
|
||||
|
||||
REASON_DISCONNECTED: {
|
||||
result: this.RESULT_DISCONNECTED,
|
||||
message: "Message manager disconnected",
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the given `data` object matches the given `filter`
|
||||
* object. The objects match if every property of `filter` is present
|
||||
* in `data`, and the values in both objects are strictly equal.
|
||||
*
|
||||
* @param {object} filter
|
||||
* The filter object to match against.
|
||||
* @param {object} data
|
||||
* The data object being matched.
|
||||
* @returns {bool} True if the objects match.
|
||||
*/
|
||||
matchesFilter(filter, data) {
|
||||
return Object.keys(filter).every(key => {
|
||||
return key in data && data[key] === filter[key];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a message listener to the given message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager on which to listen.
|
||||
* @param {string|number} messageName
|
||||
* The name of the message to listen for.
|
||||
* @param {MessageReceiver} handler
|
||||
* The handler to dispatch to. Must be an object with the following
|
||||
* properties:
|
||||
*
|
||||
* receiveMessage:
|
||||
* A method which is called for each message received by the
|
||||
* listener. The method takes one argument, an object, with the
|
||||
* following properties:
|
||||
*
|
||||
* messageName:
|
||||
* The internal message name, as passed to `sendMessage`.
|
||||
*
|
||||
* target:
|
||||
* The message manager which received this message.
|
||||
*
|
||||
* channelId:
|
||||
* The internal ID of the transaction, used to map responses to
|
||||
* the original sender.
|
||||
*
|
||||
* sender:
|
||||
* An object describing the sender, as passed to `sendMessage`.
|
||||
*
|
||||
* recipient:
|
||||
* An object describing the recipient, as passed to
|
||||
* `sendMessage`.
|
||||
*
|
||||
* data:
|
||||
* The contents of the message, as passed to `sendMessage`.
|
||||
*
|
||||
* The method may return any structured-clone-compatible
|
||||
* object, which will be returned as a response to the message
|
||||
* sender. It may also instead return a `Promise`, the
|
||||
* resolution or rejection value of which will likewise be
|
||||
* returned to the message sender.
|
||||
*
|
||||
* messageFilter:
|
||||
* An object containing arbitrary properties on which to filter
|
||||
* received messages. Messages will only be dispatched to this
|
||||
* object if the `recipient` object passed to `sendMessage`
|
||||
* matches this filter, as determined by `matchesFilter`.
|
||||
*/
|
||||
addListener(target, messageName, handler) {
|
||||
this.messageManagers.get(target).addHandler(messageName, handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a message listener from the given message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager on which to stop listening.
|
||||
* @param {string|number} messageName
|
||||
* The name of the message to stop listening for.
|
||||
* @param {MessageReceiver} handler
|
||||
* The handler to stop dispatching to.
|
||||
*/
|
||||
removeListener(target, messageName, handler) {
|
||||
this.messageManagers.get(target).removeListener(messageName, handler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a message via the given message manager. Returns a promise which
|
||||
* resolves or rejects with the return value of the message receiver.
|
||||
*
|
||||
* The promise also rejects if there is no matching listener, or the other
|
||||
* side of the message manager disconnects before the response is received.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager on which to send the message.
|
||||
* @param {string} messageName
|
||||
* The name of the message to send, as passed to `addListener`.
|
||||
* @param {object} data
|
||||
* A structured-clone-compatible object to send to the message
|
||||
* recipient.
|
||||
* @param {object} [recipient]
|
||||
* A structured-clone-compatible object to identify the message
|
||||
* recipient. The object must match the `messageFilter` defined by
|
||||
* recipients in order for the message to be received.
|
||||
* @param {object} [sender]
|
||||
* A structured-clone-compatible object to identify the message
|
||||
* sender. This object may also be used as a filter to prematurely
|
||||
* abort responses when the sender is being destroyed.
|
||||
* @see `abortResponses`.
|
||||
* @returns Promise
|
||||
*/
|
||||
sendMessage(target, messageName, data, recipient = {}, sender = {}) {
|
||||
let channelId = gChannelId++;
|
||||
let message = { messageName, channelId, sender, recipient, data };
|
||||
|
||||
let deferred = PromiseUtils.defer();
|
||||
deferred.messageFilter = {};
|
||||
deferred.sender = recipient;
|
||||
deferred.messageManager = target;
|
||||
|
||||
this._addPendingResponse(deferred);
|
||||
|
||||
// The channel ID is used as the message name when routing responses.
|
||||
// Add a message listener to the response broker, and remove it once
|
||||
// we've gotten (or canceled) a response.
|
||||
let broker = this.responseManagers.get(target);
|
||||
broker.addHandler(channelId, deferred);
|
||||
|
||||
let cleanup = () => {
|
||||
broker.removeHandler(channelId, deferred);
|
||||
};
|
||||
deferred.promise.then(cleanup, cleanup);
|
||||
|
||||
target.sendAsyncMessage(MESSAGE_MESSAGE, message);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles dispatching message callbacks from the message brokers to their
|
||||
* appropriate `MessageReceivers`, and routing the responses back to the
|
||||
* original senders.
|
||||
*
|
||||
* Each handler object is a `MessageReceiver` object as passed to
|
||||
* `addListener`.
|
||||
*/
|
||||
_handleMessage({ handler, error }, data) {
|
||||
// The target passed to `receiveMessage` is sometimes a message manager
|
||||
// owner instead of a message manager, so make sure to convert it to a
|
||||
// message manager first if necessary.
|
||||
let { target } = data;
|
||||
if (!(target instanceof Ci.nsIMessageSender)) {
|
||||
target = target.messageManager;
|
||||
}
|
||||
|
||||
let deferred = {
|
||||
sender: data.sender,
|
||||
messageManager: target,
|
||||
};
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.reject = reject;
|
||||
|
||||
if (handler) {
|
||||
let result = handler.receiveMessage(data);
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
}).then(
|
||||
value => {
|
||||
let response = {
|
||||
result: this.RESULT_SUCCESS,
|
||||
messageName: data.channelId,
|
||||
recipient: {},
|
||||
value,
|
||||
};
|
||||
|
||||
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
|
||||
},
|
||||
error => {
|
||||
let response = {
|
||||
result: this.RESULT_ERROR,
|
||||
messageName: data.channelId,
|
||||
recipient: {},
|
||||
error,
|
||||
};
|
||||
|
||||
if (error && typeof(error) == "object") {
|
||||
if (error.result) {
|
||||
response.result = error.result;
|
||||
}
|
||||
}
|
||||
|
||||
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
|
||||
});
|
||||
|
||||
this._addPendingResponse(deferred);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles message callbacks from the response brokers.
|
||||
*
|
||||
* Each handler object is a deferred object created by `sendMessage`, and
|
||||
* should be resolved or rejected based on the contents of the response.
|
||||
*/
|
||||
_handleResponse({ handler, error }, data) {
|
||||
if (error) {
|
||||
// If we have an error at this point, we have handler to report it to,
|
||||
// so just log it.
|
||||
Cu.reportError(error.message);
|
||||
} else if (data.result === this.RESULT_SUCCESS) {
|
||||
handler.resolve(data.value);
|
||||
} else {
|
||||
handler.reject(data);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a pending response to the the `pendingResponses` list.
|
||||
*
|
||||
* The response object must be a deferred promise with the following
|
||||
* properties:
|
||||
*
|
||||
* promise:
|
||||
* The promise object which resolves or rejects when the response
|
||||
* is no longer pending.
|
||||
*
|
||||
* reject:
|
||||
* A function which, when called, causes the `promise` object to be
|
||||
* rejected.
|
||||
*
|
||||
* sender:
|
||||
* A sender object, as passed to `sendMessage.
|
||||
*
|
||||
* messageManager:
|
||||
* The message manager the response will be sent or received on.
|
||||
*
|
||||
* When the promise resolves or rejects, it will be removed from the
|
||||
* list.
|
||||
*
|
||||
* These values are used to clear pending responses when execution
|
||||
* contexts are destroyed.
|
||||
*/
|
||||
_addPendingResponse(deferred) {
|
||||
let cleanup = () => {
|
||||
this.pendingResponses.delete(deferred);
|
||||
};
|
||||
this.pendingResponses.add(deferred);
|
||||
deferred.promise.then(cleanup, cleanup);
|
||||
},
|
||||
|
||||
/**
|
||||
* Aborts any pending message responses to senders matching the given
|
||||
* filter.
|
||||
*
|
||||
* @param {object} sender
|
||||
* The object on which to filter senders, as determined by
|
||||
* `matchesFilter`.
|
||||
* @param {object} [reason]
|
||||
* An optional object describing the reason the response was aborted.
|
||||
* Will be passed to the promise rejection handler of all aborted
|
||||
* responses.
|
||||
*/
|
||||
abortResponses(sender, reason = this.REASON_DISCONNECTED) {
|
||||
for (let response of this.pendingResponses) {
|
||||
if (this.matchesFilter(sender, response.sender)) {
|
||||
response.reject(reason);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Aborts any pending message responses to the broker for the given
|
||||
* message manager.
|
||||
*
|
||||
* @param {nsIMessageSender} target
|
||||
* The message manager for which to abort brokers.
|
||||
* @param {object} reason
|
||||
* An object describing the reason the responses were aborted.
|
||||
* Will be passed to the promise rejection handler of all aborted
|
||||
* responses.
|
||||
*/
|
||||
abortMessageManager(target, reason) {
|
||||
for (let response of this.pendingResponses) {
|
||||
if (response.messageManager === target) {
|
||||
response.reject(reason);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "message-manager-close":
|
||||
case "message-manager-disconnect":
|
||||
try {
|
||||
if (this.responseManagers.has(subject)) {
|
||||
this.abortMessageManager(subject, this.REASON_DISCONNECTED);
|
||||
}
|
||||
} finally {
|
||||
this.responseManagers.delete(subject);
|
||||
this.messageManagers.delete(subject);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
MessageChannel.init();
|
@ -10,6 +10,7 @@ EXTRA_JS_MODULES += [
|
||||
'ExtensionManagement.jsm',
|
||||
'ExtensionStorage.jsm',
|
||||
'ExtensionUtils.jsm',
|
||||
'MessageChannel.jsm',
|
||||
'Schemas.jsm',
|
||||
]
|
||||
|
||||
|
50
toolkit/components/telemetry/docs/core-ping.rst
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
"core" ping
|
||||
============
|
||||
|
||||
This mobile-specific ping is intended to provide the most critical
|
||||
data in a concise format, allowing for frequent uploads.
|
||||
|
||||
Since this ping is used to measure retention, it should be sent
|
||||
each time the browser is opened.
|
||||
|
||||
Submission will be per the Edge server specification::
|
||||
|
||||
/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
|
||||
|
||||
* ``docId`` is a UUID for deduping
|
||||
* ``docType`` is “core”
|
||||
* ``appName`` is “Fennec”
|
||||
* ``appVersion`` is the version of the application (e.g. "46.0a1")
|
||||
* ``appUpdateChannel`` is “release”, “beta”, etc.
|
||||
* ``appBuildID`` is the build number
|
||||
|
||||
Note: Counts below (e.g. search & usage times) are “since the last
|
||||
ping”, not total for the whole application lifetime.
|
||||
|
||||
Structure::
|
||||
|
||||
{
|
||||
"v": 1, // ping format version
|
||||
"clientId": <string>, // client id, e.g.
|
||||
// "c641eacf-c30c-4171-b403-f077724e848a"
|
||||
"seq": <positive integer>, // running ping counter, e.g. 3
|
||||
"locale": <string>, // application locale, e.g. "en-US"
|
||||
"os": <string>, // OS name.
|
||||
"osversion": <string>, // OS version.
|
||||
"device": <string>, // Build.MANUFACTURER + " - " + Build.MODEL
|
||||
// where manufacturer is truncated to 12 characters
|
||||
// & model is truncated to 19 characters
|
||||
"arch": <string>, // e.g. "arm", "x86"
|
||||
|
||||
"experiments": [<string>, …], // Optional, array of identifiers
|
||||
// for the active experiments
|
||||
}
|
||||
|
||||
The ``device`` field is filled in with information specified by the hardware
|
||||
manufacturer. As such, it could be excessively long and use excessive amounts
|
||||
of limited user data. To avoid this, we limit the length of the field. We're
|
||||
more likely have collisions for models within a manufacturer (e.g. "Galaxy S5"
|
||||
vs. "Galaxy Note") than we are for shortened manufacturer names so we provide
|
||||
more characters for the model than the manufacturer.
|
||||
|
@ -19,6 +19,7 @@ Client-side, this consists of:
|
||||
common-ping
|
||||
environment
|
||||
main-ping
|
||||
core-ping
|
||||
deletion-ping
|
||||
crash-ping
|
||||
uitour-ping
|
||||
|
@ -152,6 +152,13 @@ IMEHandler::ProcessMessage(nsWindow* aWindow, UINT aMessage,
|
||||
WPARAM& aWParam, LPARAM& aLParam,
|
||||
MSGResult& aResult)
|
||||
{
|
||||
if (aMessage == MOZ_WM_DISMISS_ONSCREEN_KEYBOARD) {
|
||||
if (!sFocusedWindow) {
|
||||
DismissOnScreenKeyboard();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef NS_ENABLE_TSF
|
||||
if (IsTSFAvailable()) {
|
||||
TSFTextStore::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult);
|
||||
@ -253,7 +260,7 @@ IMEHandler::NotifyIME(nsWindow* aWindow,
|
||||
}
|
||||
case NOTIFY_IME_OF_BLUR:
|
||||
sFocusedWindow = nullptr;
|
||||
IMEHandler::MaybeDismissOnScreenKeyboard();
|
||||
IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
|
||||
IMMHandler::OnFocusChange(false, aWindow);
|
||||
return TSFTextStore::OnFocusChange(false, aWindow,
|
||||
aWindow->GetInputContext());
|
||||
@ -302,11 +309,13 @@ IMEHandler::NotifyIME(nsWindow* aWindow,
|
||||
case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
|
||||
return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification);
|
||||
case NOTIFY_IME_OF_FOCUS:
|
||||
sFocusedWindow = aWindow;
|
||||
IMMHandler::OnFocusChange(true, aWindow);
|
||||
IMEHandler::MaybeShowOnScreenKeyboard();
|
||||
return NS_OK;
|
||||
case NOTIFY_IME_OF_BLUR:
|
||||
IMEHandler::MaybeDismissOnScreenKeyboard();
|
||||
sFocusedWindow = nullptr;
|
||||
IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
|
||||
IMMHandler::OnFocusChange(false, aWindow);
|
||||
#ifdef NS_ENABLE_TSF
|
||||
// If a plugin gets focus while TSF has focus, we need to notify TSF of
|
||||
@ -597,14 +606,15 @@ IMEHandler::MaybeShowOnScreenKeyboard()
|
||||
|
||||
// static
|
||||
void
|
||||
IMEHandler::MaybeDismissOnScreenKeyboard()
|
||||
IMEHandler::MaybeDismissOnScreenKeyboard(nsWindow* aWindow)
|
||||
{
|
||||
if (sPluginHasFocus ||
|
||||
!IsWin8OrLater()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IMEHandler::DismissOnScreenKeyboard();
|
||||
::PostMessage(aWindow->GetWindowHandle(), MOZ_WM_DISMISS_ONSCREEN_KEYBOARD,
|
||||
0, 0);
|
||||
}
|
||||
|
||||
// static
|
||||
|
@ -141,7 +141,7 @@ private:
|
||||
static bool IsIMMActive();
|
||||
|
||||
static void MaybeShowOnScreenKeyboard();
|
||||
static void MaybeDismissOnScreenKeyboard();
|
||||
static void MaybeDismissOnScreenKeyboard(nsWindow* aWindow);
|
||||
static bool WStringStartsWithCaseInsensitive(const std::wstring& aHaystack,
|
||||
const std::wstring& aNeedle);
|
||||
static bool IsKeyboardPresentOnSlate();
|
||||
|
@ -41,6 +41,8 @@
|
||||
#define MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE (WM_APP+0x0315)
|
||||
// Internal message used in correcting backwards clock skew
|
||||
#define MOZ_WM_SKEWFIX (WM_APP+0x0316)
|
||||
// Internal message used for hiding the on-screen keyboard
|
||||
#define MOZ_WM_DISMISS_ONSCREEN_KEYBOARD (WM_APP+0x0317)
|
||||
|
||||
// Internal message for ensuring the file picker is visible on multi monitor
|
||||
// systems, and when the screen resolution changes.
|
||||
|