diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /b2g/chrome/content | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'b2g/chrome/content')
47 files changed, 6197 insertions, 0 deletions
diff --git a/b2g/chrome/content/ErrorPage.js b/b2g/chrome/content/ErrorPage.js new file mode 100644 index 000000000..d51782466 --- /dev/null +++ b/b2g/chrome/content/ErrorPage.js @@ -0,0 +1,73 @@ +/* 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'; + +var Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; + +dump("############ ErrorPage.js\n"); + +var ErrorPageHandler = { + _reload: function() { + docShell.QueryInterface(Ci.nsIWebNavigation).reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); + }, + + _certErrorPageEventHandler: function(e) { + let target = e.originalTarget; + let errorDoc = target.ownerDocument; + + // If the event came from an ssl error page, it is one of the "Add + // Exception…" buttons. + if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) { + let permanent = errorDoc.getElementById("permanentExceptionButton"); + let temp = errorDoc.getElementById("temporaryExceptionButton"); + if (target == temp || target == permanent) { + sendAsyncMessage("ErrorPage:AddCertException", { + url: errorDoc.location.href, + isPermanent: target == permanent + }); + } + } + }, + + _bindPageEvent: function(target) { + if (!target) { + return; + } + + if (/^about:certerror/.test(target.documentURI)) { + let errorPageEventHandler = this._certErrorPageEventHandler.bind(this); + addEventListener("click", errorPageEventHandler, true, false); + let listener = function() { + removeEventListener("click", errorPageEventHandler, true); + removeEventListener("pagehide", listener, true); + }.bind(this); + + addEventListener("pagehide", listener, true); + } + }, + + domContentLoadedHandler: function(e) { + let target = e.originalTarget; + let targetDocShell = target.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + if (targetDocShell != docShell) { + return; + } + this._bindPageEvent(target); + }, + + init: function() { + addMessageListener("ErrorPage:ReloadPage", this._reload.bind(this)); + addEventListener('DOMContentLoaded', + this.domContentLoadedHandler.bind(this), + true); + this._bindPageEvent(content.document); + } +}; + +ErrorPageHandler.init(); diff --git a/b2g/chrome/content/aboutCertError.xhtml b/b2g/chrome/content/aboutCertError.xhtml new file mode 100644 index 000000000..616657e54 --- /dev/null +++ b/b2g/chrome/content/aboutCertError.xhtml @@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % certerrorDTD + SYSTEM "chrome://b2g-l10n/locale/aboutCertError.dtd"> + %certerrorDTD; +]> + +<!-- 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/. --> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&certerror.pagetitle;</title> + <meta name="viewport" content="width=device-width; user-scalable=false" /> + <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" /> + <!-- This page currently uses the same favicon as neterror.xhtml. + If the location of the favicon is changed for both pages, the + FAVICON_ERRORPAGE_URL symbol in toolkit/components/places/src/nsFaviconService.h + should be updated. If this page starts using a different favicon + than neterrorm nsFaviconService->SetAndLoadFaviconForPage + should be updated to ignore this one as well. --> + <link rel="icon" type="image/png" id="favicon" sizes="64x64" href="chrome://global/skin/icons/warning-64.png"/> + + <script type="application/javascript"><![CDATA[ + // Error url MUST be formatted like this: + // about:certerror?e=error&u=url&d=desc + + // Note that this file uses document.documentURI to get + // the URL (with the format from above). This is because + // document.location.href gets the current URI off the docshell, + // which is the URL displayed in the location bar, i.e. + // the URI that the user attempted to load. + + function getCSSClass() + { + var url = document.documentURI; + var matches = url.match(/s\=([^&]+)\&/); + // s is optional, if no match just return nothing + if (!matches || matches.length < 2) + return ""; + + // parenthetical match is the second entry + return decodeURIComponent(matches[1]); + } + + function getDescription() + { + var url = document.documentURI; + var desc = url.search(/d\=/); + + // desc == -1 if not found; if so, return an empty string + // instead of what would turn out to be portions of the URI + if (desc == -1) + return ""; + + return decodeURIComponent(url.slice(desc + 2)); + } + + function initPage() + { + // Replace the "#1" string in the intro with the hostname. Trickier + // than it might seem since we want to preserve the <b> tags, but + // not allow for any injection by just using innerHTML. Instead, + // just find the right target text node. + var intro = document.getElementById('introContentP1'); + function replaceWithHost(node) { + if (node.textContent == "#1") + node.textContent = location.host; + else + for(var i = 0; i < node.childNodes.length; i++) + replaceWithHost(node.childNodes[i]); + }; + replaceWithHost(intro); + + if (getCSSClass() == "expertBadCert") { + toggle('technicalContent'); + toggle('expertContent'); + } + + var tech = document.getElementById("technicalContentText"); + if (tech) + tech.textContent = getDescription(); + + addDomainErrorLink(); + } + + /* In the case of SSL error pages about domain mismatch, see if + we can hyperlink the user to the correct site. We don't want + to do this generically since it allows MitM attacks to redirect + users to a site under attacker control, but in certain cases + it is safe (and helpful!) to do so. Bug 402210 + */ + function addDomainErrorLink() { + // Rather than textContent, we need to treat description as HTML + var sd = document.getElementById("technicalContentText"); + if (sd) { + var desc = getDescription(); + + // sanitize description text - see bug 441169 + + // First, find the index of the <a> tag we care about, being careful not to + // use an over-greedy regex + var re = /<a id="cert_domain_link" title="([^"]+)">/; + var result = re.exec(desc); + if(!result) + return; + + // Remove sd's existing children + sd.textContent = ""; + + // Everything up to the link should be text content + sd.appendChild(document.createTextNode(desc.slice(0, result.index))); + + // Now create the link itself + var anchorEl = document.createElement("a"); + anchorEl.setAttribute("id", "cert_domain_link"); + anchorEl.setAttribute("title", result[1]); + anchorEl.appendChild(document.createTextNode(result[1])); + sd.appendChild(anchorEl); + + // Finally, append text for anything after the closing </a> + sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length))); + } + + var link = document.getElementById('cert_domain_link'); + if (!link) + return; + + var okHost = link.getAttribute("title"); + var thisHost = document.location.hostname; + var proto = document.location.protocol; + + // If okHost is a wildcard domain ("*.example.com") let's + // use "www" instead. "*.example.com" isn't going to + // get anyone anywhere useful. bug 432491 + okHost = okHost.replace(/^\*\./, "www."); + + /* case #1: + * example.com uses an invalid security certificate. + * + * The certificate is only valid for www.example.com + * + * Make sure to include the "." ahead of thisHost so that + * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com" + * + * We'd normally just use a RegExp here except that we lack a + * library function to escape them properly (bug 248062), and + * domain names are famous for having '.' characters in them, + * which would allow spurious and possibly hostile matches. + */ + if (endsWith(okHost, "." + thisHost)) + link.href = proto + okHost; + + /* case #2: + * browser.garage.maemo.org uses an invalid security certificate. + * + * The certificate is only valid for garage.maemo.org + */ + if (endsWith(thisHost, "." + okHost)) + link.href = proto + okHost; + + // If we set a link, meaning there's something helpful for + // the user here, expand the section by default + if (link.href && getCSSClass() != "expertBadCert") + toggle("technicalContent"); + } + + function endsWith(haystack, needle) { + return haystack.slice(-needle.length) == needle; + } + + function toggle(id) { + var el = document.getElementById(id); + if (el.getAttribute("collapsed")) + el.setAttribute("collapsed", false); + else + el.setAttribute("collapsed", true); + } + ]]></script> + </head> + + <body id="errorPage" class="certerror" dir="&locale.dir;"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 class="errorTitleText">&certerror.longpagetitle;</h1> + </div> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + <div id="introContent"> + <p id="introContentP1">&certerror.introPara1;</p> + </div> + + <!-- The following sections can be unhidden by default by setting the + "browser.xul.error_pages.expert_bad_cert" pref to true --> + <div id="technicalContent" collapsed="true"> + <h2 onclick="toggle('technicalContent');" id="technicalContentHeading">&certerror.technical.heading;</h2> + <p id="technicalContentText"/> + </div> + + <div id="expertContent" collapsed="true"> + <h2 onclick="toggle('expertContent');" id="expertContentHeading">&certerror.expert.heading;</h2> + <div> + <p>&certerror.expert.content;</p> + <p>&certerror.expert.contentPara2;</p> + <button id="temporaryExceptionButton">&certerror.addTemporaryException.label;</button> + <button id="permanentExceptionButton">&certerror.addPermanentException.label;</button> + </div> + </div> + </div> + </div> + + <!-- + - Note: It is important to run the script this way, instead of using + - an onload handler. This is because error pages are loaded as + - LOAD_BACKGROUND, which means that onload handlers will not be executed. + --> + <script type="application/javascript">initPage();</script> + + </body> +</html> diff --git a/b2g/chrome/content/arrow.svg b/b2g/chrome/content/arrow.svg new file mode 100644 index 000000000..d3d9e8246 --- /dev/null +++ b/b2g/chrome/content/arrow.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> + +<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="11px" style="position: absolute; top: -moz-calc(50% - 2px);"> + <polyline points="1 1 5 6 9 1" stroke="#414141" stroke-width="2" stroke-linecap="round" fill="transparent" stroke-linejoin="round"/> +</svg> diff --git a/b2g/chrome/content/blank.css b/b2g/chrome/content/blank.css new file mode 100644 index 000000000..71914be1f --- /dev/null +++ b/b2g/chrome/content/blank.css @@ -0,0 +1,7 @@ +/* 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/. */ + +body { + background: black; +} diff --git a/b2g/chrome/content/blank.html b/b2g/chrome/content/blank.html new file mode 100644 index 000000000..b8b20e2c6 --- /dev/null +++ b/b2g/chrome/content/blank.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<!-- 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/. --> + +<html> + <head> + <link rel="stylesheet" href="blank.css" type="text/css" media="all" /> + </head> +</html> diff --git a/b2g/chrome/content/content.css b/b2g/chrome/content/content.css new file mode 100644 index 000000000..bb478087e --- /dev/null +++ b/b2g/chrome/content/content.css @@ -0,0 +1,321 @@ +/* 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/. */ + +@namespace url("http://www.w3.org/1999/xhtml"); +@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + +/* Style the scrollbars */ +xul|window xul|scrollbar { + display: none; +} + +/* Bug 1041576 - Scrollable with scrollgrab should not have scrollbars */ + +@-moz-document domain(system.gaiamobile.org) { + .browser-container > xul|scrollbar { + display: none; + } +} + +%ifdef MOZ_GRAPHENE +.moz-noscrollbars > xul|scrollbar { + display: none; +} +%endif + +xul|scrollbar[root="true"] { + position: relative; + z-index: 2147483647; +} + +xul|scrollbar { + -moz-appearance: none !important; + background-color: transparent !important; + background-image: none !important; + border: 0px solid transparent !important; + pointer-events: none; +} + +/* Scrollbar code will reset the margin to the correct side depending on + where layout actually puts the scrollbar */ +xul|scrollbar[orient="vertical"] { + margin-left: -8px; + min-width: 8px; + max-width: 8px; +} + +xul|scrollbar[orient="vertical"] xul|thumb { + max-width: 6px !important; + min-width: 6px !important; +} + +xul|scrollbar[orient="horizontal"] { + margin-top: -8px; + min-height: 8px; + max-height: 8px; +} + +xul|scrollbar[orient="horizontal"] xul|thumb { + max-height: 6px !important; + min-height: 6px !important; +} + +xul|scrollbar:not([active="true"]), +xul|scrollbar[disabled] { + opacity: 0; +} + +xul|scrollbarbutton { + min-height: 8px !important; + min-width: 8px !important; + -moz-appearance: none !important; + visibility: hidden; +} + +xul|scrollbarbutton[sbattr="scrollbar-up-top"], +xul|scrollbarbutton[sbattr="scrollbar-bottom-top"] { + display: none; +} + +xul|thumb { + background-color: rgba(0, 0, 0, 0.4) !important; + -moz-border-top-colors: none !important; + -moz-border-bottom-colors: none !important; + -moz-border-right-colors: none !important; + -moz-border-left-colors: none !important; + border: 1px solid rgba(255, 255, 255, 0.4) !important; + border-radius: 3px; +} + +xul|scrollbarbutton { + background-image: none !important; +} + +%ifndef MOZ_GRAPHENE +/* -moz-touch-enabled? media elements */ +:-moz-any(video, audio) > xul|videocontrols { + -moz-binding: url("chrome://global/content/bindings/videocontrols.xml#touchControlsGonk"); +} + +select:not([size]):not([multiple]) > xul|scrollbar, +select[size="1"] > xul|scrollbar, +select:not([size]):not([multiple]) xul|scrollbarbutton, +select[size="1"] xul|scrollbarbutton { + display: block; + margin-left: 0; + min-width: 16px; +} + +/* Override inverse OS themes */ +select, +textarea, +button, +xul|button, +* > input:not([type="image"]) { + -moz-appearance: none !important; /* See bug 598421 for fixing the platform */ + border-radius: 3px; +} + +select[size], +select[multiple], +select[size][multiple], +textarea, +* > input:not([type="image"]) { + border-style: solid; + border-color: #7d7d7d; + color: #414141; + background-color: white; +} + +/* Selects are handled by the form helper, see bug 685197 */ +select option, select optgroup { + pointer-events: none; +} + +select:not([size]):not([multiple]), +select[size="0"], +select[size="1"], +* > input[type="button"], +* > input[type="submit"], +* > input[type="reset"], +button { + border-style: solid; + border-color: #7d7d7d; + color: #414141; + background: white linear-gradient(rgba(255,255,255,0.2) 0, rgba(215,215,215,0.5) 18px, rgba(115,115,115,0.5) 100%); +} + +input[type="checkbox"] { + background-color: white; +} + +input[type="radio"] { + background-color: white; +} + +select { + border-width: 1px; + padding: 1px; +} + +select:not([size]):not([multiple]), +select[size="0"], +select[size="1"] { + padding: 0 1px 0 1px; +} + +* > input:not([type="image"]) { + border-width: 1px; + padding: 1px; +} + +textarea { + resize: none; + border-width: 1px; + padding-inline-start: 1px; + padding-inline-end: 1px; + padding-block-start: 2px; + padding-block-end: 2px; +} + +input[type="button"], +input[type="submit"], +input[type="reset"], +button { + border-width: 1px; + padding-inline-start: 7px; + padding-inline-end: 7px; + padding-block-start: 0; + padding-block-end: 0; +} + +input[type="radio"], +input[type="checkbox"] { + border: 1px solid #a7a7a7 !important; + padding-inline-start: 1px; + padding-inline-end: 1px; + padding-block-start: 2px; + padding-block-end: 2px; +} + +select > button { + border-width: 0px !important; + margin: 0px !important; + padding: 0px !important; + border-radius: 0; + color: #414141; + + background-image: radial-gradient(at bottom left, #bbbbbb 40%, #f5f5f5), url(arrow.svg) !important; + background-color: transparent; + background-position: -15px center, 4px center !important; + background-repeat: no-repeat, no-repeat !important; + background-size: 100% 90%, auto auto; + + -moz-binding: none !important; + position: relative !important; + font-size: inherit; +} + +select[size]:focus, +select[multiple]:focus, +select[size][multiple]:focus, +textarea:focus, +input[type="file"]:focus > input[type="text"], +* > input:not([type="image"]):focus { + outline: 0px !important; + border-style: solid; + border-color: rgb(94,128,153); + background-color: white; +} + +select:not([size]):not([multiple]):focus, +select[size="0"]:focus, +select[size="1"]:focus, +input[type="button"]:focus, +input[type="submit"]:focus, +input[type="reset"]:focus, +button:focus { + outline: 0px !important; + border-style: solid; + border-color: rgb(94,128,153); + background: white linear-gradient(rgba(255,255,255,0.2) 0, rgba(198,225,256,0.2) 18px, rgba(27,113,177,0.5) 100%); +} + +input[type="checkbox"]:focus, +input[type="radio"]:focus { + border-color: #99c6e0 !important; +} + +/* we need to be specific for selects because the above rules are specific too */ +textarea[disabled], +select[size][disabled], +select[multiple][disabled], +select[size][multiple][disabled], +select:not([size]):not([multiple])[disabled], +select[size="0"][disabled], +select[size="1"][disabled], +button[disabled], +* > input:not([type="image"])[disabled] { + color: rgba(0,0,0,0.3); + border-color: rgba(125,125,125,0.4); + border-style: solid; + border-width: 1px; + background-color: #f5f5f5; +} + +select:not([size]):not([multiple])[disabled], +select[size="0"][disabled], +select[size="1"][disabled] { + background-color: #f5f5f5; +} + +input[type="button"][disabled], +input[type="submit"][disabled], +input[type="reset"][disabled], +button[disabled] { + padding-inline-start: 7px; + padding-inline-end: 7px; + padding-block-start: 0; + padding-block-end: 0; + background-color: #f5f5f5; +} + +input[type="radio"][disabled], +input[type="radio"][disabled]:active, +input[type="radio"][disabled]:hover, +input[type="radio"][disabled]:hover:active, +input[type="checkbox"][disabled], +input[type="checkbox"][disabled]:active, +input[type="checkbox"][disabled]:hover, +input[type="checkbox"][disabled]:hover:active { + border:1px solid rgba(125,125,125,0.4) !important; +} + +select[disabled] > button { + opacity: 0.6; + padding: 1px 7px 1px 7px; +} + +*:any-link:active, +*[role=button]:active, +button:active, +option:active, +select:active, +label:active { + background-color: rgba(141, 184, 216, 0.5); +} + +input[type=number] > div > div, /* work around bug 946184 */ +input[type=number]::-moz-number-spin-box { + display: none; +} +%endif + +%ifdef MOZ_WIDGET_GONK +/* This binding only provide key shortcuts that we can't use on devices */ +input, +textarea { +-moz-binding: none !important; +} +%endif diff --git a/b2g/chrome/content/desktop.css b/b2g/chrome/content/desktop.css new file mode 100644 index 000000000..9612d732c --- /dev/null +++ b/b2g/chrome/content/desktop.css @@ -0,0 +1,59 @@ +#controls { + position: absolute; + left: 0; + bottom:0; + right: 0; + height: 30px; + background-color: -moz-dialog; +} + +#home-button { + margin: auto; + margin-top: 3px; + width: 24px; + height: 24px; + background: #eee url("images/desktop/home-black.png") center no-repeat; + border: 1px solid #888; + border-radius: 12px; + display: block; +} + +#home-button::-moz-focus-inner { + padding: 0; + border: 0; +} + +#home-button:hover { + background-image: url("images/desktop/home-white.png"); + background-color: #ccc; + border-color: #555; +} + +#home-button.active { + background-image: url("images/desktop/home-white.png"); + background-color: #888; + border-color: black; +} + +#rotate-button { + position: absolute; + top: 3px; + bottom: 3px; + right: 3px; + width: 24px; + height: 24px; + background: #eee url("images/desktop/rotate.png") center no-repeat; + border: 1px solid #888; + border-radius: 12px; + display: block; +} + +#rotate-button:hover { + background-color: #ccc; + border-color: #555; +} + +#rotate-button.active { + background-color: #888; + border-color: black; +} diff --git a/b2g/chrome/content/desktop.js b/b2g/chrome/content/desktop.js new file mode 100644 index 000000000..5a1e7ff04 --- /dev/null +++ b/b2g/chrome/content/desktop.js @@ -0,0 +1,179 @@ +/* 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/. */ + +var browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); +var isMulet = "ResponsiveUI" in browserWindow; + +// Enable touch event shim on desktop that translates mouse events +// into touch ones +function enableTouch() { + let require = Cu.import('resource://devtools/shared/Loader.jsm', {}) + .devtools.require; + let { TouchEventSimulator } = require('devtools/shared/touch/simulator'); + let touchEventSimulator = new TouchEventSimulator(shell.contentBrowser); + touchEventSimulator.start(); +} + +// Some additional buttons are displayed on simulators to fake hardware buttons. +function setupButtons() { + let link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.href = 'chrome://b2g/content/desktop.css'; + document.head.appendChild(link); + + let footer = document.createElement('footer'); + footer.id = 'controls'; + document.body.appendChild(footer); + let homeButton = document.createElement('button'); + homeButton.id = 'home-button'; + footer.appendChild(homeButton); + let rotateButton = document.createElement('button'); + rotateButton.id = 'rotate-button'; + footer.appendChild(rotateButton); + + homeButton.addEventListener('mousedown', function() { + let window = shell.contentBrowser.contentWindow; + let e = new window.KeyboardEvent('keydown', {key: 'Home'}); + window.dispatchEvent(e); + homeButton.classList.add('active'); + }); + homeButton.addEventListener('mouseup', function() { + let window = shell.contentBrowser.contentWindow; + let e = new window.KeyboardEvent('keyup', {key: 'Home'}); + window.dispatchEvent(e); + homeButton.classList.remove('active'); + }); + + Cu.import("resource://gre/modules/GlobalSimulatorScreen.jsm"); + rotateButton.addEventListener('mousedown', function() { + rotateButton.classList.add('active'); + }); + rotateButton.addEventListener('mouseup', function() { + GlobalSimulatorScreen.flipScreen(); + rotateButton.classList.remove('active'); + }); +} + +function setupStorage() { + let directory = null; + + // Get the --storage-path argument from the command line. + try { + let service = Cc['@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds'].getService(Ci.nsISupports); + let args = service.wrappedJSObject.cmdLine; + if (args) { + let path = args.handleFlagWithParam('storage-path', false); + directory = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile); + directory.initWithPath(path); + } + } catch(e) { + directory = null; + } + + // Otherwise, default to 'storage' folder within current profile. + if (!directory) { + directory = Services.dirsvc.get('ProfD', Ci.nsIFile); + directory.append('storage'); + if (!directory.exists()) { + directory.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("755", 8)); + } + } + dump("Set storage path to: " + directory.path + "\n"); + + // This is the magic, where we override the default location for the storages. + Services.prefs.setCharPref('device.storage.overrideRootDir', directory.path); +} + +function checkDebuggerPort() { + // XXX: To be removed once bug 942756 lands. + // We are hacking 'unix-domain-socket' pref by setting a tcp port (number). + // SocketListener.open detects that it isn't a file path (string), and starts + // listening on the tcp port given here as command line argument. + + // Get the command line arguments that were passed to the b2g client + let args; + try { + let service = Cc["@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds"].getService(Ci.nsISupports); + args = service.wrappedJSObject.cmdLine; + } catch(e) {} + + if (!args) { + return; + } + + let dbgport; + try { + dbgport = args.handleFlagWithParam('start-debugger-server', false); + } catch(e) {} + + if (dbgport) { + dump('Opening debugger server on ' + dbgport + '\n'); + Services.prefs.setCharPref('devtools.debugger.unix-domain-socket', dbgport); + navigator.mozSettings.createLock().set( + {'debugger.remote-mode': 'adb-devtools'}); + } +} + + +function initResponsiveDesign() { + Cu.import('resource://devtools/client/responsivedesign/responsivedesign.jsm'); + ResponsiveUIManager.on('on', function(event, {tab:tab}) { + let responsive = ResponsiveUIManager.getResponsiveUIForTab(tab); + let document = tab.ownerDocument; + + // Only tweak reponsive mode for shell.html tabs. + if (tab.linkedBrowser.contentWindow != window) { + return; + } + + // Disable transition as they mess up with screen size handler + responsive.transitionsEnabled = false; + + responsive.buildPhoneUI(); + + responsive.rotatebutton.addEventListener('command', function (evt) { + GlobalSimulatorScreen.flipScreen(); + evt.stopImmediatePropagation(); + evt.preventDefault(); + }, true); + + // Enable touch events + responsive.enableTouch(); + }); + + + let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager; + mgr.toggle(browserWindow, browserWindow.gBrowser.selectedTab); + +} + +function openDevtools() { + // Open devtool panel while maximizing its size according to screen size + Services.prefs.setIntPref('devtools.toolbox.sidebar.width', + browserWindow.outerWidth - 550); + Services.prefs.setCharPref('devtools.toolbox.host', 'side'); + let {gDevTools} = Cu.import('resource://devtools/client/framework/gDevTools.jsm', {}); + let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + let target = devtools.TargetFactory.forTab(browserWindow.gBrowser.selectedTab); + gDevTools.showToolbox(target); +} + +window.addEventListener('ContentStart', function() { + // On Firefox Mulet, touch events are enabled within the responsive mode + if (!isMulet) { + enableTouch(); + } + if (Services.prefs.getBoolPref('b2g.software-buttons')) { + setupButtons(); + } + checkDebuggerPort(); + setupStorage(); + // On Firefox mulet, we automagically enable the responsive mode + // and show the devtools + if (isMulet) { + initResponsiveDesign(browserWindow); + openDevtools(); + } +}); diff --git a/b2g/chrome/content/devtools/adb.js b/b2g/chrome/content/devtools/adb.js new file mode 100644 index 000000000..cebc6696b --- /dev/null +++ b/b2g/chrome/content/devtools/adb.js @@ -0,0 +1,233 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This file is only loaded on Gonk to manage ADB state + +Components.utils.import("resource://gre/modules/FileUtils.jsm"); + +const DEBUG = false; +var debug = function(str) { + dump("AdbController: " + str + "\n"); +} + +var AdbController = { + locked: undefined, + remoteDebuggerEnabled: undefined, + lockEnabled: undefined, + disableAdbTimer: null, + disableAdbTimeoutHours: 12, + umsActive: false, + + setLockscreenEnabled: function(value) { + this.lockEnabled = value; + DEBUG && debug("setLockscreenEnabled = " + this.lockEnabled); + this.updateState(); + }, + + setLockscreenState: function(value) { + this.locked = value; + DEBUG && debug("setLockscreenState = " + this.locked); + this.updateState(); + }, + + setRemoteDebuggerState: function(value) { + this.remoteDebuggerEnabled = value; + DEBUG && debug("setRemoteDebuggerState = " + this.remoteDebuggerEnabled); + this.updateState(); + }, + + startDisableAdbTimer: function() { + if (this.disableAdbTimer) { + this.disableAdbTimer.cancel(); + } else { + this.disableAdbTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + try { + this.disableAdbTimeoutHours = + Services.prefs.getIntPref("b2g.adb.timeout-hours"); + } catch (e) { + // This happens if the pref doesn't exist, in which case + // disableAdbTimeoutHours will still be set to the default. + } + } + if (this.disableAdbTimeoutHours <= 0) { + DEBUG && debug("Timer to disable ADB not started due to zero timeout"); + return; + } + + DEBUG && debug("Starting timer to disable ADB in " + + this.disableAdbTimeoutHours + " hours"); + let timeoutMilliseconds = this.disableAdbTimeoutHours * 60 * 60 * 1000; + this.disableAdbTimer.initWithCallback(this, timeoutMilliseconds, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + stopDisableAdbTimer: function() { + DEBUG && debug("Stopping timer to disable ADB"); + if (this.disableAdbTimer) { + this.disableAdbTimer.cancel(); + this.disableAdbTimer = null; + } + }, + + notify: function(aTimer) { + if (aTimer == this.disableAdbTimer) { + this.disableAdbTimer = null; + // The following dump will be the last thing that shows up in logcat, + // and will at least give the user a clue about why logcat was + // disconnected, if the user happens to be using logcat. + debug("ADB timer expired - disabling ADB\n"); + navigator.mozSettings.createLock().set( + {'debugger.remote-mode': 'disabled'}); + } + }, + + updateState: function() { + this.umsActive = false; + }, + + updateStateInternal: function() { + DEBUG && debug("updateStateInternal: called"); + + if (this.remoteDebuggerEnabled === undefined || + this.lockEnabled === undefined || + this.locked === undefined) { + // Part of initializing the settings database will cause the observers + // to trigger. We want to wait until both have been initialized before + // we start changing ther adb state. Without this then we can wind up + // toggling adb off and back on again (or on and back off again). + // + // For completeness, one scenario which toggles adb is using the unagi. + // The unagi has adb enabled by default (prior to b2g starting). If you + // have the phone lock disabled and remote debugging enabled, then we'll + // receive an unlock event and an rde event. However at the time we + // receive the unlock event we haven't yet received the rde event, so + // we turn adb off momentarily, which disconnects a logcat that might + // be running. Changing the defaults (in AdbController) just moves the + // problem to a different phone, which has adb disabled by default and + // we wind up turning on adb for a short period when we shouldn't. + // + // By waiting until both values are properly initialized, we avoid + // turning adb on or off accidentally. + DEBUG && debug("updateState: Waiting for all vars to be initialized"); + return; + } + + // Check if we have a remote debugging session going on. If so, we won't + // disable adb even if the screen is locked. + let isDebugging = USBRemoteDebugger.isDebugging; + DEBUG && debug("isDebugging=" + isDebugging); + + // If USB Mass Storage, USB tethering, or a debug session is active, + // then we don't want to disable adb in an automatic fashion (i.e. + // when the screen locks or due to timeout). + let sysUsbConfig = libcutils.property_get("sys.usb.config").split(","); + let usbFuncActive = this.umsActive || isDebugging; + usbFuncActive |= (sysUsbConfig.indexOf("rndis") >= 0); + usbFuncActive |= (sysUsbConfig.indexOf("mtp") >= 0); + + let enableAdb = this.remoteDebuggerEnabled && + (!(this.lockEnabled && this.locked) || usbFuncActive); + + let useDisableAdbTimer = true; + try { + if (Services.prefs.getBoolPref("marionette.defaultPrefs.enabled")) { + // Marionette is enabled. Marionette requires that adb be on (and also + // requires that remote debugging be off). The fact that marionette + // is enabled also implies that we're doing a non-production build, so + // we want adb enabled all of the time. + enableAdb = true; + useDisableAdbTimer = false; + } + } catch (e) { + // This means that the pref doesn't exist. Which is fine. We just leave + // enableAdb alone. + } + + // Check wakelock to prevent adb from disconnecting when phone is locked + let lockFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile); + lockFile.initWithPath('/sys/power/wake_lock'); + if(lockFile.exists()) { + let foStream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + let coStream = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); + let str = {}; + foStream.init(lockFile, FileUtils.MODE_RDONLY, 0, 0); + coStream.init(foStream, "UTF-8", 0, 0); + coStream.readString(-1, str); + coStream.close(); + foStream.close(); + let wakeLockContents = str.value.replace(/\n/, ""); + let wakeLockList = wakeLockContents.split(" "); + if (wakeLockList.indexOf("adb") >= 0) { + enableAdb = true; + useDisableAdbTimer = false; + DEBUG && debug("Keeping ADB enabled as ADB wakelock is present."); + } else { + DEBUG && debug("ADB wakelock not found."); + } + } else { + DEBUG && debug("Wake_lock file not found."); + } + + DEBUG && debug("updateState: enableAdb = " + enableAdb + + " remoteDebuggerEnabled = " + this.remoteDebuggerEnabled + + " lockEnabled = " + this.lockEnabled + + " locked = " + this.locked + + " usbFuncActive = " + usbFuncActive); + + // Configure adb. + let currentConfig = libcutils.property_get("persist.sys.usb.config"); + let configFuncs = currentConfig.split(","); + if (currentConfig == "" || currentConfig == "none") { + // We want to treat none like the empty string. + // "".split(",") yields [""] and not [] + configFuncs = []; + } + let adbIndex = configFuncs.indexOf("adb"); + + if (enableAdb) { + // Add adb to the list of functions, if not already present + if (adbIndex < 0) { + configFuncs.push("adb"); + } + } else { + // Remove adb from the list of functions, if present + if (adbIndex >= 0) { + configFuncs.splice(adbIndex, 1); + } + } + let newConfig = configFuncs.join(","); + if (newConfig == "") { + // Convert the empty string back into none, since that's what init.rc + // needs. + newConfig = "none"; + } + if (newConfig != currentConfig) { + DEBUG && debug("updateState: currentConfig = " + currentConfig); + DEBUG && debug("updateState: newConfig = " + newConfig); + try { + libcutils.property_set("persist.sys.usb.config", newConfig); + } catch(e) { + Cu.reportError("Error configuring adb: " + e); + } + } + if (useDisableAdbTimer) { + if (enableAdb && !usbFuncActive) { + this.startDisableAdbTimer(); + } else { + this.stopDisableAdbTimer(); + } + } + } +}; + +SettingsListener.observe("lockscreen.locked", false, + AdbController.setLockscreenState.bind(AdbController)); +SettingsListener.observe("lockscreen.enabled", false, + AdbController.setLockscreenEnabled.bind(AdbController)); diff --git a/b2g/chrome/content/devtools/debugger.js b/b2g/chrome/content/devtools/debugger.js new file mode 100644 index 000000000..11987a839 --- /dev/null +++ b/b2g/chrome/content/devtools/debugger.js @@ -0,0 +1,397 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "devtools", function() { + const { devtools } = + Cu.import("resource://devtools/shared/Loader.jsm", {}); + return devtools; +}); + +XPCOMUtils.defineLazyGetter(this, "DebuggerServer", function() { + const { DebuggerServer } = devtools.require("devtools/server/main"); + return DebuggerServer; +}); + +XPCOMUtils.defineLazyGetter(this, "B2GTabList", function() { + const { B2GTabList } = + devtools.require("resource://gre/modules/DebuggerActors.js"); + return B2GTabList; +}); + +// Load the discovery module eagerly, so that it can set a device name at +// startup. This does not cause discovery to start listening for packets, as +// that only happens once DevTools is enabled. +devtools.require("devtools/shared/discovery/discovery"); + +var RemoteDebugger = { + _listening: false, + + /** + * Prompt the user to accept or decline the incoming connection. + * + * @param session object + * The session object will contain at least the following fields: + * { + * authentication, + * client: { + * host, + * port + * }, + * server: { + * host, + * port + * } + * } + * Specific authentication modes may include additional fields. Check + * the different |allowConnection| methods in + * devtools/shared/security/auth.js. + * @return An AuthenticationResult value. + * A promise that will be resolved to the above is also allowed. + */ + allowConnection(session) { + if (this._promptingForAllow) { + // Don't stack connection prompts if one is already open + return DebuggerServer.AuthenticationResult.DENY; + } + this._listen(); + + this._promptingForAllow = new Promise(resolve => { + this._handleAllowResult = detail => { + this._handleAllowResult = null; + this._promptingForAllow = null; + // Newer Gaia supplies |authResult|, which is one of the + // AuthenticationResult values. + if (detail.authResult) { + resolve(detail.authResult); + } else if (detail.value) { + resolve(DebuggerServer.AuthenticationResult.ALLOW); + } else { + resolve(DebuggerServer.AuthenticationResult.DENY); + } + }; + + shell.sendChromeEvent({ + type: "remote-debugger-prompt", + session + }); + }); + + return this._promptingForAllow; + }, + + /** + * During OOB_CERT authentication, the user must transfer some data through some + * out of band mechanism from the client to the server to authenticate the + * devices. + * + * This implementation instructs Gaia to continually capture images which are + * passed back here and run through a QR decoder. + * + * @return An object containing: + * * sha256: hash(ClientCert) + * * k : K(random 128-bit number) + * A promise that will be resolved to the above is also allowed. + */ + receiveOOB() { + if (this._receivingOOB) { + return this._receivingOOB; + } + this._listen(); + + const QR = devtools.require("devtools/shared/qrcode/index"); + this._receivingOOB = new Promise((resolve, reject) => { + this._handleAuthEvent = detail => { + debug(detail.action); + if (detail.action === "abort") { + this._handleAuthEvent = null; + this._receivingOOB = null; + reject(); + return; + } + + if (detail.action !== "capture") { + return; + } + + let url = detail.url; + QR.decodeFromURI(url).then(data => { + debug("Got auth data: " + data); + let oob = JSON.parse(data); + + shell.sendChromeEvent({ + type: "devtools-auth", + action: "stop" + }); + + this._handleAuthEvent = null; + this._receivingOOB = null; + resolve(oob); + }).catch(() => { + debug("No auth data, requesting new capture"); + shell.sendChromeEvent({ + type: "devtools-auth", + action: "capture" + }); + }); + }; + + // Show QR scanning dialog, get an initial capture + shell.sendChromeEvent({ + type: "devtools-auth", + action: "start" + }); + }); + + return this._receivingOOB; + }, + + _listen: function() { + if (this._listening) { + return; + } + + this.handleEvent = this.handleEvent.bind(this); + let content = shell.contentBrowser.contentWindow; + content.addEventListener("mozContentEvent", this, false, true); + this._listening = true; + }, + + handleEvent: function(event) { + let detail = event.detail; + if (detail.type === "remote-debugger-prompt" && this._handleAllowResult) { + this._handleAllowResult(detail); + } + if (detail.type === "devtools-auth" && this._handleAuthEvent) { + this._handleAuthEvent(detail); + } + }, + + initServer: function() { + if (DebuggerServer.initialized) { + return; + } + + // Ask for remote connections. + DebuggerServer.init(); + + // /!\ Be careful when adding a new actor, especially global actors. + // Any new global actor will be exposed and returned by the root actor. + + // Add Firefox-specific actors, but prevent tab actors to be loaded in + // the parent process, unless we enable certified apps debugging. + let restrictPrivileges = Services.prefs.getBoolPref("devtools.debugger.forbid-certified-apps"); + DebuggerServer.addBrowserActors("navigator:browser", restrictPrivileges); + + // Allow debugging of chrome for any process + if (!restrictPrivileges) { + DebuggerServer.allowChromeProcess = true; + } + + /** + * Construct a root actor appropriate for use in a server running in B2G. + * The returned root actor respects the factories registered with + * DebuggerServer.addGlobalActor only if certified apps debugging is on, + * otherwise we used an explicit limited list of global actors + * + * * @param connection DebuggerServerConnection + * The conection to the client. + */ + DebuggerServer.createRootActor = function createRootActor(connection) + { + let parameters = { + tabList: new B2GTabList(connection), + // Use an explicit global actor list to prevent exposing + // unexpected actors + globalActorFactories: restrictPrivileges ? { + webappsActor: DebuggerServer.globalActorFactories.webappsActor, + deviceActor: DebuggerServer.globalActorFactories.deviceActor, + settingsActor: DebuggerServer.globalActorFactories.settingsActor + } : DebuggerServer.globalActorFactories + }; + let { RootActor } = devtools.require("devtools/server/actors/root"); + let root = new RootActor(connection, parameters); + root.applicationType = "operating-system"; + return root; + }; + + if (isGonk) { + DebuggerServer.on("connectionchange", function() { + AdbController.updateState(); + }); + } + } +}; + +RemoteDebugger.allowConnection = + RemoteDebugger.allowConnection.bind(RemoteDebugger); +RemoteDebugger.receiveOOB = + RemoteDebugger.receiveOOB.bind(RemoteDebugger); + +var USBRemoteDebugger = { + + get isDebugging() { + if (!this._listener) { + return false; + } + + return DebuggerServer._connections && + Object.keys(DebuggerServer._connections).length > 0; + }, + + start: function() { + if (this._listener) { + return; + } + + RemoteDebugger.initServer(); + + let portOrPath = + Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") || + "/data/local/debugger-socket"; + + try { + debug("Starting USB debugger on " + portOrPath); + let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT"); + let authenticator = new AuthenticatorType.Server(); + authenticator.allowConnection = RemoteDebugger.allowConnection; + this._listener = DebuggerServer.createListener(); + this._listener.portOrPath = portOrPath; + this._listener.authenticator = authenticator; + this._listener.open(); + // Temporary event, until bug 942756 lands and offers a way to know + // when the server is up and running. + Services.obs.notifyObservers(null, "debugger-server-started", null); + } catch (e) { + debug("Unable to start USB debugger server: " + e); + } + }, + + stop: function() { + if (!this._listener) { + return; + } + + try { + this._listener.close(); + this._listener = null; + } catch (e) { + debug("Unable to stop USB debugger server: " + e); + } + } + +}; + +var WiFiRemoteDebugger = { + + start: function() { + if (this._listener) { + return; + } + + RemoteDebugger.initServer(); + + try { + debug("Starting WiFi debugger"); + let AuthenticatorType = DebuggerServer.Authenticators.get("OOB_CERT"); + let authenticator = new AuthenticatorType.Server(); + authenticator.allowConnection = RemoteDebugger.allowConnection; + authenticator.receiveOOB = RemoteDebugger.receiveOOB; + this._listener = DebuggerServer.createListener(); + this._listener.portOrPath = -1 /* any available port */; + this._listener.authenticator = authenticator; + this._listener.discoverable = true; + this._listener.encryption = true; + this._listener.open(); + let port = this._listener.port; + debug("Started WiFi debugger on " + port); + } catch (e) { + debug("Unable to start WiFi debugger server: " + e); + } + }, + + stop: function() { + if (!this._listener) { + return; + } + + try { + this._listener.close(); + this._listener = null; + } catch (e) { + debug("Unable to stop WiFi debugger server: " + e); + } + } + +}; + +(function() { + // Track these separately here so we can determine the correct value for the + // pref "devtools.debugger.remote-enabled", which is true when either mode of + // using DevTools is enabled. + let devtoolsUSB = false; + let devtoolsWiFi = false; + + // Keep the old setting to not break people that won't have updated + // gaia and gecko. + SettingsListener.observe("devtools.debugger.remote-enabled", false, + function(value) { + devtoolsUSB = value; + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", + devtoolsUSB || devtoolsWiFi); + // This preference is consulted during startup + Services.prefs.savePrefFile(null); + try { + value ? USBRemoteDebugger.start() : USBRemoteDebugger.stop(); + } catch(e) { + dump("Error while initializing USB devtools: " + + e + "\n" + e.stack + "\n"); + } + }); + + SettingsListener.observe("debugger.remote-mode", "disabled", function(value) { + if (["disabled", "adb-only", "adb-devtools"].indexOf(value) == -1) { + dump("Illegal value for debugger.remote-mode: " + value + "\n"); + return; + } + + devtoolsUSB = value == "adb-devtools"; + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", + devtoolsUSB || devtoolsWiFi); + // This preference is consulted during startup + Services.prefs.savePrefFile(null); + + try { + (value == "adb-devtools") ? USBRemoteDebugger.start() + : USBRemoteDebugger.stop(); + } catch(e) { + dump("Error while initializing USB devtools: " + + e + "\n" + e.stack + "\n"); + } + + isGonk && AdbController.setRemoteDebuggerState(value != "disabled"); + }); + + SettingsListener.observe("devtools.remote.wifi.enabled", false, + function(value) { + devtoolsWiFi = value; + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", + devtoolsUSB || devtoolsWiFi); + // Allow remote debugging on non-local interfaces when WiFi debug is enabled + // TODO: Bug 1034411: Lock down to WiFi interface, instead of all interfaces + Services.prefs.setBoolPref("devtools.debugger.force-local", !value); + // This preference is consulted during startup + Services.prefs.savePrefFile(null); + + try { + value ? WiFiRemoteDebugger.start() : WiFiRemoteDebugger.stop(); + } catch(e) { + dump("Error while initializing WiFi devtools: " + + e + "\n" + e.stack + "\n"); + } + }); +})(); diff --git a/b2g/chrome/content/devtools/hud.js b/b2g/chrome/content/devtools/hud.js new file mode 100644 index 000000000..64e9d553d --- /dev/null +++ b/b2g/chrome/content/devtools/hud.js @@ -0,0 +1,1017 @@ +/* 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'; + +// settings.js loads this file when the HUD setting is enabled. + +const DEVELOPER_HUD_LOG_PREFIX = 'DeveloperHUD'; +const CUSTOM_HISTOGRAM_PREFIX = 'DEVTOOLS_HUD_CUSTOM_'; +const APPNAME_IDX = 3; +const HISTNAME_IDX = 4; + +XPCOMUtils.defineLazyGetter(this, 'devtools', function() { + const {devtools} = Cu.import('resource://devtools/shared/Loader.jsm', {}); + return devtools; +}); + +XPCOMUtils.defineLazyGetter(this, 'DebuggerClient', function() { + return devtools.require('devtools/shared/client/main').DebuggerClient; +}); + +XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() { + return devtools.require('devtools/shared/webconsole/utils').Utils; +}); + +XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() { + return devtools.require('devtools/shared/fronts/eventlooplag').EventLoopLagFront; +}); + +XPCOMUtils.defineLazyGetter(this, 'PerformanceEntriesFront', function() { + return devtools.require('devtools/server/actors/performance-entries').PerformanceEntriesFront; +}); + +XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() { + return devtools.require('devtools/server/actors/memory').MemoryFront; +}); + +Cu.import('resource://gre/modules/Frames.jsm'); + +var _telemetryDebug = false; + +function telemetryDebug(...args) { + if (_telemetryDebug) { + args.unshift('[AdvancedTelemetry]'); + console.log(...args); + } +} + +/** + * The Developer HUD is an on-device developer tool that displays widgets, + * showing visual debug information about apps. Each widget corresponds to a + * metric as tracked by a metric watcher (e.g. consoleWatcher). + */ +var developerHUD = { + + _targets: new Map(), + _histograms: new Set(), + _customHistograms: new Set(), + _client: null, + _conn: null, + _watchers: [], + _logging: true, + _telemetry: false, + + /** + * This method registers a metric watcher that will watch one or more metrics + * on app frames that are being tracked. A watcher must implement the + * `trackTarget(target)` and `untrackTarget(target)` methods, register + * observed metrics with `target.register(metric)`, and keep them up-to-date + * with `target.update(metric, message)` when necessary. + */ + registerWatcher(watcher) { + this._watchers.unshift(watcher); + }, + + init() { + if (this._client) { + return; + } + + if (!DebuggerServer.initialized) { + RemoteDebugger.initServer(); + } + + // We instantiate a local debugger connection so that watchers can use our + // DebuggerClient to send requests to tab actors (e.g. the consoleActor). + // Note the special usage of the private _serverConnection, which we need + // to call connectToChild and set up child process actors on a frame we + // intend to track. These actors will use the connection to communicate with + // our DebuggerServer in the parent process. + let transport = DebuggerServer.connectPipe(); + this._conn = transport._serverConnection; + this._client = new DebuggerClient(transport); + + for (let w of this._watchers) { + if (w.init) { + w.init(this._client); + } + } + + Frames.addObserver(this); + + let appFrames = Frames.list().filter(frame => frame.getAttribute('mozapp')); + for (let frame of appFrames) { + this.trackFrame(frame); + } + + SettingsListener.observe('hud.logging', this._logging, enabled => { + this._logging = enabled; + }); + + SettingsListener.observe('hud.telemetry.logging', _telemetryDebug, enabled => { + _telemetryDebug = enabled; + }); + + SettingsListener.observe('metrics.selectedMetrics.level', "", level => { + this._telemetry = (level === 'Enhanced'); + }); + }, + + uninit() { + if (!this._client) { + return; + } + + for (let frame of this._targets.keys()) { + this.untrackFrame(frame); + } + + Frames.removeObserver(this); + + this._client.close(); + delete this._client; + }, + + /** + * This method will ask all registered watchers to track and update metrics + * on an app frame. + */ + trackFrame(frame) { + if (this._targets.has(frame)) { + return; + } + + DebuggerServer.connectToChild(this._conn, frame).then(actor => { + let target = new Target(frame, actor); + this._targets.set(frame, target); + + for (let w of this._watchers) { + w.trackTarget(target); + } + }); + }, + + untrackFrame(frame) { + let target = this._targets.get(frame); + if (target) { + for (let w of this._watchers) { + w.untrackTarget(target); + } + + target.destroy(); + this._targets.delete(frame); + } + }, + + onFrameCreated(frame, isFirstAppFrame) { + let mozapp = frame.getAttribute('mozapp'); + if (!mozapp) { + return; + } + this.trackFrame(frame); + }, + + onFrameDestroyed(frame, isLastAppFrame) { + let mozapp = frame.getAttribute('mozapp'); + if (!mozapp) { + return; + } + this.untrackFrame(frame); + }, + + log(message) { + if (this._logging) { + dump(DEVELOPER_HUD_LOG_PREFIX + ': ' + message + '\n'); + } + } + +}; + + +/** + * A Target object represents all there is to know about a Firefox OS app frame + * that is being tracked, e.g. a pointer to the frame, current values of watched + * metrics, and how to notify the front-end when metrics have changed. + */ +function Target(frame, actor) { + this.frame = frame; + this.actor = actor; + this.metrics = new Map(); + this._appName = null; +} + +Target.prototype = { + + get manifest() { + return this.frame.appManifestURL; + }, + + get appName() { + + if (this._appName) { + return this._appName; + } + + let manifest = this.manifest; + if (!manifest) { + let msg = DEVELOPER_HUD_LOG_PREFIX + ': Unable to determine app for telemetry metric. src: ' + + this.frame.src; + console.error(msg); + return null; + } + + // "communications" apps are a special case + if (manifest.indexOf('communications') === -1) { + let start = manifest.indexOf('/') + 2; + let end = manifest.indexOf('.', start); + this._appName = manifest.substring(start, end).toLowerCase(); + } else { + let src = this.frame.src; + if (src) { + // e.g., `app://communications.gaiamobile.org/contacts/index.html` + let parts = src.split('/'); + let APP = 3; + let EXPECTED_PARTS_LENGTH = 5; + if (parts.length === EXPECTED_PARTS_LENGTH) { + this._appName = parts[APP]; + } + } + } + + return this._appName; + }, + + /** + * Register a metric that can later be updated. Does not update the front-end. + */ + register(metric) { + this.metrics.set(metric, 0); + }, + + /** + * Modify one of a target's metrics, and send out an event to notify relevant + * parties (e.g. the developer HUD, automated tests, etc). + */ + update(metric, message) { + if (!metric.name) { + throw new Error('Missing metric.name'); + } + + if (!metric.value) { + metric.value = 0; + } + + let metrics = this.metrics; + if (metrics) { + metrics.set(metric.name, metric.value); + } + + let data = { + metrics: [], // FIXME(Bug 982066) Remove this field. + manifest: this.manifest, + metric: metric, + message: message + }; + + // FIXME(Bug 982066) Remove this loop. + if (metrics && metrics.size > 0) { + for (let name of metrics.keys()) { + data.metrics.push({name: name, value: metrics.get(name)}); + } + } + + if (message) { + developerHUD.log('[' + data.manifest + '] ' + data.message); + } + + this._send(data); + }, + + /** + * Nicer way to call update() when the metric value is a number that needs + * to be incremented. + */ + bump(metric, message) { + metric.value = (this.metrics.get(metric.name) || 0) + 1; + this.update(metric, message); + }, + + /** + * Void a metric value and make sure it isn't displayed on the front-end + * anymore. + */ + clear(metric) { + metric.value = 0; + this.update(metric); + }, + + /** + * Tear everything down, including the front-end by sending a message without + * widgets. + */ + destroy() { + delete this.metrics; + this._send({metric: {skipTelemetry: true}}); + }, + + _send(data) { + let frame = this.frame; + + shell.sendEvent(frame, 'developer-hud-update', Cu.cloneInto(data, frame)); + this._logHistogram(data.metric); + }, + + _getAddonHistogram(item) { + let appName = this._getAddonHistogramName(item, APPNAME_IDX); + let histName = this._getAddonHistogramName(item, HISTNAME_IDX); + + return Services.telemetry.getAddonHistogram(appName, CUSTOM_HISTOGRAM_PREFIX + + histName); + }, + + _getAddonHistogramName(item, index) { + let array = item.split('_'); + return array[index].toUpperCase(); + }, + + _clearTelemetryData() { + developerHUD._histograms.forEach(function(item) { + Services.telemetry.getKeyedHistogramById(item).clear(); + }); + + developerHUD._customHistograms.forEach(item => { + this._getAddonHistogram(item).clear(); + }); + }, + + _sendTelemetryData() { + if (!developerHUD._telemetry) { + return; + } + telemetryDebug('calling sendTelemetryData'); + let frame = this.frame; + let payload = { + keyedHistograms: {}, + addonHistograms: {} + }; + // Package the hud histograms. + developerHUD._histograms.forEach(function(item) { + payload.keyedHistograms[item] = + Services.telemetry.getKeyedHistogramById(item).snapshot(); + }); + + // Package the registered hud custom histograms + developerHUD._customHistograms.forEach(item => { + let appName = this._getAddonHistogramName(item, APPNAME_IDX); + let histName = CUSTOM_HISTOGRAM_PREFIX + + this._getAddonHistogramName(item, HISTNAME_IDX); + let addonHist = Services.telemetry.getAddonHistogram(appName, histName).snapshot(); + if (!(appName in payload.addonHistograms)) { + payload.addonHistograms[appName] = {}; + } + // Do not include histograms with sum of 0. + if (addonHist.sum > 0) { + payload.addonHistograms[appName][histName] = addonHist; + } + }); + shell.sendEvent(frame, 'advanced-telemetry-update', Cu.cloneInto(payload, frame)); + }, + + _logHistogram(metric) { + if (!developerHUD._telemetry || metric.skipTelemetry) { + return; + } + + metric.appName = this.appName; + if (!metric.appName) { + return; + } + + let metricName = metric.name.toUpperCase(); + let metricAppName = metric.appName.toUpperCase(); + if (!metric.custom) { + let keyedMetricName = 'DEVTOOLS_HUD_' + metricName; + try { + let keyed = Services.telemetry.getKeyedHistogramById(keyedMetricName); + if (keyed) { + keyed.add(metric.appName, parseInt(metric.value, 10)); + developerHUD._histograms.add(keyedMetricName); + telemetryDebug(keyedMetricName, metric.value, metric.appName); + } + } catch(err) { + console.error('Histogram error is metricname added to histograms.json:' + + keyedMetricName); + } + } else { + let histogramName = CUSTOM_HISTOGRAM_PREFIX + metricAppName + '_' + + metricName; + // This is a call to add a value to an existing histogram. + if (typeof metric.value !== 'undefined') { + Services.telemetry.getAddonHistogram(metricAppName, + CUSTOM_HISTOGRAM_PREFIX + metricName).add(parseInt(metric.value, 10)); + telemetryDebug(histogramName, metric.value); + return; + } + + // The histogram already exists and are not adding data to it. + if (developerHUD._customHistograms.has(histogramName)) { + return; + } + + // This is a call to create a new histogram. + try { + let metricType = parseInt(metric.type, 10); + if (metricType === Services.telemetry.HISTOGRAM_COUNT) { + Services.telemetry.registerAddonHistogram(metricAppName, + CUSTOM_HISTOGRAM_PREFIX + metricName, metricType); + } else { + Services.telemetry.registerAddonHistogram(metricAppName, + CUSTOM_HISTOGRAM_PREFIX + metricName, metricType, metric.min, + metric.max, metric.buckets); + } + developerHUD._customHistograms.add(histogramName); + } catch (err) { + console.error('Histogram error: ' + err); + } + } + } +}; + + +/** + * The Console Watcher tracks the following metrics in apps: reflows, warnings, + * and errors, with security errors reported separately. + */ +var consoleWatcher = { + + _client: null, + _targets: new Map(), + _watching: { + reflows: false, + warnings: false, + errors: false, + security: false + }, + _security: [ + 'Mixed Content Blocker', + 'Mixed Content Message', + 'CSP', + 'Invalid HSTS Headers', + 'Invalid HPKP Headers', + 'Insecure Password Field', + 'SSL', + 'CORS' + ], + _reflowThreshold: 0, + + init(client) { + this._client = client; + this.consoleListener = this.consoleListener.bind(this); + + let watching = this._watching; + + for (let key in watching) { + let metric = key; + SettingsListener.observe('hud.' + metric, watching[metric], watch => { + // Watch or unwatch the metric. + if (watching[metric] = watch) { + return; + } + + // If unwatched, remove any existing widgets for that metric. + for (let target of this._targets.values()) { + target.clear({name: metric}); + } + }); + } + + SettingsListener.observe('hud.reflows.duration', this._reflowThreshold, threshold => { + this._reflowThreshold = threshold; + }); + + client.addListener('logMessage', this.consoleListener); + client.addListener('pageError', this.consoleListener); + client.addListener('consoleAPICall', this.consoleListener); + client.addListener('reflowActivity', this.consoleListener); + }, + + trackTarget(target) { + target.register('reflows'); + target.register('warnings'); + target.register('errors'); + target.register('security'); + + this._client.request({ + to: target.actor.consoleActor, + type: 'startListeners', + listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity'] + }, (res) => { + this._targets.set(target.actor.consoleActor, target); + }); + }, + + untrackTarget(target) { + this._client.request({ + to: target.actor.consoleActor, + type: 'stopListeners', + listeners: ['LogMessage', 'PageError', 'ConsoleAPI', 'ReflowActivity'] + }, (res) => { }); + + this._targets.delete(target.actor.consoleActor); + }, + + consoleListener(type, packet) { + let target = this._targets.get(packet.from); + let metric = {}; + let output = ''; + + switch (packet.type) { + + case 'pageError': + let pageError = packet.pageError; + + if (pageError.warning || pageError.strict) { + metric.name = 'warnings'; + output += 'Warning ('; + } else { + metric.name = 'errors'; + output += 'Error ('; + } + + if (this._security.indexOf(pageError.category) > -1) { + metric.name = 'security'; + + // Telemetry sends the security error category not the + // count of security errors. + target._logHistogram({ + name: 'security_category', + value: pageError.category + }); + + // Indicate that the 'hud' security metric (the count of security + // errors) should not be sent as a telemetry metric since the + // security error category is being sent instead. + metric.skipTelemetry = true; + } + + let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError; + output += category + '): "' + (errorMessage.initial || errorMessage) + + '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber; + break; + + case 'consoleAPICall': + switch (packet.message.level) { + + case 'error': + metric.name = 'errors'; + output += 'Error (console)'; + break; + + case 'warn': + metric.name = 'warnings'; + output += 'Warning (console)'; + break; + + case 'info': + this.handleTelemetryMessage(target, packet); + + // Currently, informational log entries are tracked only by + // telemetry. Nonetheless, for consistency, we continue here + // and let the function return normally, when it concludes 'info' + // entries are not being watched. + metric.name = 'info'; + break; + + default: + return; + } + break; + + case 'reflowActivity': + metric.name = 'reflows'; + + let {start, end, sourceURL, interruptible} = packet; + metric.interruptible = interruptible; + let duration = Math.round((end - start) * 100) / 100; + + // Record the reflow if the duration exceeds the threshold. + if (duration < this._reflowThreshold) { + return; + } + + output += 'Reflow: ' + duration + 'ms'; + if (sourceURL) { + output += ' ' + this.formatSourceURL(packet); + } + + // Telemetry also records reflow duration. + target._logHistogram({ + name: 'reflow_duration', + value: Math.round(duration) + }); + break; + + default: + return; + } + + if (developerHUD._telemetry) { + // Always record telemetry for these metrics. + if (metric.name === 'errors' || metric.name === 'warnings' || metric.name === 'reflows') { + let value = target.metrics.get(metric.name); + metric.value = (value || 0) + 1; + target._logHistogram(metric); + + // Telemetry has already been recorded. + metric.skipTelemetry = true; + + // If the metric is not being watched, persist the incremented value. + // If the metric is being watched, `target.bump` will increment the value + // of the metric and will persist the incremented value. + if (!this._watching[metric.name]) { + target.metrics.set(metric.name, metric.value); + } + } + } + + if (!this._watching[metric.name]) { + return; + } + + target.bump(metric, output); + }, + + formatSourceURL(packet) { + // Abbreviate source URL + let source = WebConsoleUtils.abbreviateSourceURL(packet.sourceURL); + + // Add function name and line number + let {functionName, sourceLine} = packet; + source = 'in ' + (functionName || '<anonymousFunction>') + + ', ' + source + ':' + sourceLine; + + return source; + }, + + handleTelemetryMessage(target, packet) { + if (!developerHUD._telemetry) { + return; + } + + // If this is a 'telemetry' log entry, create a telemetry metric from + // the log content. + let separator = '|'; + let logContent = packet.message.arguments.toString(); + + if (logContent.indexOf('telemetry') < 0) { + return; + } + + let telemetryData = logContent.split(separator); + + // Positions of the components of a telemetry log entry. + let TELEMETRY_IDENTIFIER_IDX = 0; + let NAME_IDX = 1; + let VALUE_IDX = 2; + let TYPE_IDX = 2; + let MIN_IDX = 3; + let MAX_IDX = 4; + let BUCKETS_IDX = 5; + let MAX_CUSTOM_ARGS = 6; + let MIN_CUSTOM_ARGS = 3; + + if (telemetryData[TELEMETRY_IDENTIFIER_IDX] != 'telemetry' || + telemetryData.length < MIN_CUSTOM_ARGS || + telemetryData.length > MAX_CUSTOM_ARGS) { + return; + } + + let metric = { + name: telemetryData[NAME_IDX] + }; + + if (metric.name === 'MGMT') { + metric.value = telemetryData[VALUE_IDX]; + if (metric.value === 'TIMETOSHIP') { + telemetryDebug('Received a Ship event'); + target._sendTelemetryData(); + } else if (metric.value === 'CLEARMETRICS') { + target._clearTelemetryData(); + } + } else { + if (telemetryData.length === MIN_CUSTOM_ARGS) { + metric.value = telemetryData[VALUE_IDX]; + } else if (telemetryData.length === MAX_CUSTOM_ARGS) { + metric.type = telemetryData[TYPE_IDX]; + metric.min = telemetryData[MIN_IDX]; + metric.max = telemetryData[MAX_IDX]; + metric.buckets = telemetryData[BUCKETS_IDX]; + } + metric.custom = true; + target._logHistogram(metric); + } + } +}; +developerHUD.registerWatcher(consoleWatcher); + + +var eventLoopLagWatcher = { + _client: null, + _fronts: new Map(), + _active: false, + + init(client) { + this._client = client; + + SettingsListener.observe('hud.jank', false, this.settingsListener.bind(this)); + }, + + settingsListener(value) { + if (this._active == value) { + return; + } + + this._active = value; + + // Toggle the state of existing fronts. + let fronts = this._fronts; + for (let target of fronts.keys()) { + if (value) { + fronts.get(target).start(); + } else { + fronts.get(target).stop(); + target.clear({name: 'jank'}); + } + } + }, + + trackTarget(target) { + target.register('jank'); + + let front = new EventLoopLagFront(this._client, target.actor); + this._fronts.set(target, front); + + front.on('event-loop-lag', time => { + target.update({name: 'jank', value: time}, 'Jank: ' + time + 'ms'); + }); + + if (this._active) { + front.start(); + } + }, + + untrackTarget(target) { + let fronts = this._fronts; + if (fronts.has(target)) { + fronts.get(target).destroy(); + fronts.delete(target); + } + } +}; +developerHUD.registerWatcher(eventLoopLagWatcher); + +/* + * The performanceEntriesWatcher determines the delta between the epoch + * of an app's launch time and the epoch of the app's performance entry marks. + * When it receives an "appLaunch" performance entry mark it records the + * name of the app being launched and the epoch of when the launch ocurred. + * When it receives subsequent performance entry events for the app being + * launched, it records the delta of the performance entry opoch compared + * to the app-launch epoch and emits an "app-start-time-<performance mark name>" + * event containing the delta. + * + * Additionally, while recording the "app-start-time" for a performance mark, + * USS memory at the time of the performance mark is also recorded. + */ +var performanceEntriesWatcher = { + _client: null, + _fronts: new Map(), + _appLaunch: new Map(), + _supported: [ + 'contentInteractive', + 'navigationInteractive', + 'navigationLoaded', + 'visuallyLoaded', + 'fullyLoaded', + 'mediaEnumerated', + 'scanEnd' + ], + + init(client) { + this._client = client; + let setting = 'devtools.telemetry.supported_performance_marks'; + let defaultValue = this._supported.join(','); + + SettingsListener.observe(setting, defaultValue, supported => { + this._supported = supported.split(','); + }); + }, + + trackTarget(target) { + // The performanceEntries watcher doesn't register a metric because + // currently the metrics generated are not displayed in + // in the front-end. + + let front = new PerformanceEntriesFront(this._client, target.actor); + this._fronts.set(target, front); + + // User timings are always gathered; there is no setting to enable/ + // disable. + front.start(); + + front.on('entry', detail => { + + // Only process performance marks. + if (detail.type !== 'mark') { + return; + } + + let name = detail.name; + let epoch = detail.epoch; + + // If this is an "app launch" mark, record the app that was + // launched and the epoch of when it was launched. + if (name.indexOf('appLaunch') !== -1) { + let CHARS_UNTIL_APP_NAME = 7; // '@app://' + let startPos = name.indexOf('@app') + CHARS_UNTIL_APP_NAME; + let endPos = name.indexOf('.'); + let appName = name.slice(startPos, endPos); + this._appLaunch.set(appName, epoch); + return; + } + + // Only process supported performance marks + if (this._supported.indexOf(name) === -1) { + return; + } + + let origin = detail.origin; + origin = origin.slice(0, origin.indexOf('.')); + + let appLaunchTime = this._appLaunch.get(origin); + + // Sanity check: ensure we have an app launch time for the app + // corresponding to this performance mark. + if (!appLaunchTime) { + return; + } + + let time = epoch - appLaunchTime; + let eventName = 'app_startup_time_' + name; + + // Events based on performance marks are for telemetry only, they are + // not displayed in the HUD front end. + target._logHistogram({name: eventName, value: time}); + + memoryWatcher.front(target).residentUnique().then(value => { + // bug 1215277, need 'v2' for app-memory histograms + eventName = 'app_memory_' + name + '_v2'; + target._logHistogram({name: eventName, value: value}); + }, err => { + console.error(err); + }); + }); + }, + + untrackTarget(target) { + let fronts = this._fronts; + if (fronts.has(target)) { + fronts.get(target).destroy(); + fronts.delete(target); + } + } +}; +developerHUD.registerWatcher(performanceEntriesWatcher); + +/** + * The Memory Watcher uses devtools actors to track memory usage. + */ +var memoryWatcher = { + + _client: null, + _fronts: new Map(), + _timers: new Map(), + _watching: { + uss: false, + appmemory: false, + jsobjects: false, + jsstrings: false, + jsother: false, + dom: false, + style: false, + other: false + }, + _active: false, + + init(client) { + this._client = client; + let watching = this._watching; + + for (let key in watching) { + let category = key; + SettingsListener.observe('hud.' + category, false, watch => { + watching[category] = watch; + this.update(); + }); + } + }, + + update() { + let watching = this._watching; + let active = watching.appmemory || watching.uss; + + if (this._active) { + for (let target of this._fronts.keys()) { + if (!watching.appmemory) target.clear({name: 'memory'}); + if (!watching.uss) target.clear({name: 'uss'}); + if (!active) clearTimeout(this._timers.get(target)); + } + } else if (active) { + for (let target of this._fronts.keys()) { + this.measure(target); + } + } + this._active = active; + }, + + measure(target) { + let watch = this._watching; + let format = this.formatMemory; + + if (watch.uss) { + this.front(target).residentUnique().then(value => { + target.update({name: 'uss', value: value}, 'USS: ' + format(value)); + }, err => { + console.error(err); + }); + } + + if (watch.appmemory) { + front.measure().then(data => { + let total = 0; + let details = []; + + function item(name, condition, value) { + if (!condition) { + return; + } + + let v = parseInt(value); + total += v; + details.push(name + ': ' + format(v)); + } + + item('JS objects', watch.jsobjects, data.jsObjectsSize); + item('JS strings', watch.jsstrings, data.jsStringsSize); + item('JS other', watch.jsother, data.jsOtherSize); + item('DOM', watch.dom, data.domSize); + item('Style', watch.style, data.styleSize); + item('Other', watch.other, data.otherSize); + // TODO Also count images size (bug #976007). + + target.update({name: 'memory', value: total}, + 'App Memory: ' + format(total) + ' (' + details.join(', ') + ')'); + }, err => { + console.error(err); + }); + } + + let timer = setTimeout(() => this.measure(target), 2000); + this._timers.set(target, timer); + }, + + formatMemory(bytes) { + var prefix = ['','K','M','G','T','P','E','Z','Y']; + var i = 0; + for (; bytes > 1024 && i < prefix.length; ++i) { + bytes /= 1024; + } + return (Math.round(bytes * 100) / 100) + ' ' + prefix[i] + 'B'; + }, + + trackTarget(target) { + target.register('uss'); + target.register('memory'); + this._fronts.set(target, MemoryFront(this._client, target.actor)); + if (this._active) { + this.measure(target); + } + }, + + untrackTarget(target) { + let front = this._fronts.get(target); + if (front) { + front.destroy(); + clearTimeout(this._timers.get(target)); + this._fronts.delete(target); + this._timers.delete(target); + } + }, + + front(target) { + return this._fronts.get(target); + } +}; +developerHUD.registerWatcher(memoryWatcher); diff --git a/b2g/chrome/content/identity.js b/b2g/chrome/content/identity.js new file mode 100644 index 000000000..9c0ad50a2 --- /dev/null +++ b/b2g/chrome/content/identity.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This JS shim contains the callbacks to fire DOMRequest events for +// navigator.pay API within the payment processor's scope. + +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); + +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +XPCOMUtils.defineLazyModuleGetter(this, "Logger", + "resource://gre/modules/identity/LogUtils.jsm"); + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["injected identity.js"].concat(aMessageArgs)); +} + +log("\n\n======================= identity.js =======================\n\n"); + +// This script may be injected more than once into an iframe. +// It's hard to do this with |const| like we should, so use var instead. +if (typeof kIdentityJSLoaded === 'undefined') { + var kIdentityDelegateWatch = "identity-delegate-watch"; + var kIdentityDelegateRequest = "identity-delegate-request"; + var kIdentityDelegateLogout = "identity-delegate-logout"; + var kIdentityDelegateReady = "identity-delegate-ready"; + var kIdentityDelegateFinished = "identity-delegate-finished"; + var kIdentityControllerDoMethod = "identity-controller-doMethod"; + var kIdentktyJSLoaded = true; +} + +var showUI = false; +var options = {}; +var isLoaded = false; +var func = null; + +/* + * Message back to the SignInToWebsite pipe. Message should be an + * object with the following keys: + * + * method: one of 'login', 'logout', 'ready' + * assertion: optional assertion + */ +function identityCall(message) { + if (options._internal) { + message._internal = options._internal; + } + sendAsyncMessage(kIdentityControllerDoMethod, message); +} + +/* + * To close the dialog, we first tell the gecko SignInToWebsite manager that it + * can clean up. Then we tell the gaia component that we are finished. It is + * necessary to notify gecko first, so that the message can be sent before gaia + * destroys our context. + */ +function closeIdentityDialog() { + // tell gecko we're done. + func = null; options = null; + sendAsyncMessage(kIdentityDelegateFinished); +} + +/* + * doInternalWatch - call the internal.watch api and relay the results + * up to the controller. + */ +function doInternalWatch() { + log("doInternalWatch:", options, isLoaded); + if (options && isLoaded) { + let BrowserID = content.wrappedJSObject.BrowserID; + BrowserID.internal.watch(function(aParams, aInternalParams) { + identityCall(aParams); + if (aParams.method === "ready") { + closeIdentityDialog(); + } + }, + JSON.stringify(options), + function(...things) { + // internal watch log callback + log("(watch) internal: ", things); + } + ); + } +} + +function doInternalRequest() { + log("doInternalRequest:", options && isLoaded); + if (options && isLoaded) { + var stringifiedOptions = JSON.stringify(options); + content.wrappedJSObject.BrowserID.internal.get( + options.origin, + function(assertion, internalParams) { + internalParams = internalParams || {}; + if (assertion) { + identityCall({ + method: 'login', + assertion: assertion, + _internalParams: internalParams}); + } else { + identityCall({ + method: 'cancel' + }); + } + closeIdentityDialog(); + }, + stringifiedOptions); + } +} +function doInternalLogout(aOptions) { + log("doInternalLogout:", (options && isLoaded)); + if (options && isLoaded) { + let BrowserID = content.wrappedJSObject.BrowserID; + BrowserID.internal.logout(options.origin, function() { + identityCall({method:'logout'}); + closeIdentityDialog(); + }); + } +} + +addEventListener("DOMContentLoaded", function(e) { + content.addEventListener("load", function(e) { + isLoaded = true; + // bring da func + if (func) func(); + }); +}); + +// listen for request +addMessageListener(kIdentityDelegateRequest, function(aMessage) { + log("injected identity.js received", kIdentityDelegateRequest); + options = aMessage.json; + showUI = true; + func = doInternalRequest; + func(); +}); + +// listen for watch +addMessageListener(kIdentityDelegateWatch, function(aMessage) { + log("injected identity.js received", kIdentityDelegateWatch); + options = aMessage.json; + showUI = false; + func = doInternalWatch; + func(); +}); + +// listen for logout +addMessageListener(kIdentityDelegateLogout, function(aMessage) { + log("injected identity.js received", kIdentityDelegateLogout); + options = aMessage.json; + showUI = false; + func = doInternalLogout; + func(); +}); diff --git a/b2g/chrome/content/images/arrowdown-16.png b/b2g/chrome/content/images/arrowdown-16.png Binary files differnew file mode 100644 index 000000000..c982426f2 --- /dev/null +++ b/b2g/chrome/content/images/arrowdown-16.png diff --git a/b2g/chrome/content/images/arrowright-16.png b/b2g/chrome/content/images/arrowright-16.png Binary files differnew file mode 100644 index 000000000..859e98ba6 --- /dev/null +++ b/b2g/chrome/content/images/arrowright-16.png diff --git a/b2g/chrome/content/images/desktop/home-black.png b/b2g/chrome/content/images/desktop/home-black.png Binary files differnew file mode 100644 index 000000000..c51187ed4 --- /dev/null +++ b/b2g/chrome/content/images/desktop/home-black.png diff --git a/b2g/chrome/content/images/desktop/home-white.png b/b2g/chrome/content/images/desktop/home-white.png Binary files differnew file mode 100644 index 000000000..43379d0e9 --- /dev/null +++ b/b2g/chrome/content/images/desktop/home-white.png diff --git a/b2g/chrome/content/images/desktop/rotate.png b/b2g/chrome/content/images/desktop/rotate.png Binary files differnew file mode 100644 index 000000000..9da1b5674 --- /dev/null +++ b/b2g/chrome/content/images/desktop/rotate.png diff --git a/b2g/chrome/content/images/error.png b/b2g/chrome/content/images/error.png Binary files differnew file mode 100644 index 000000000..58e37283a --- /dev/null +++ b/b2g/chrome/content/images/error.png diff --git a/b2g/chrome/content/images/errorpage-larry-black.png b/b2g/chrome/content/images/errorpage-larry-black.png Binary files differnew file mode 100644 index 000000000..9f2e4a6e7 --- /dev/null +++ b/b2g/chrome/content/images/errorpage-larry-black.png diff --git a/b2g/chrome/content/images/errorpage-larry-white.png b/b2g/chrome/content/images/errorpage-larry-white.png Binary files differnew file mode 100644 index 000000000..fc153c731 --- /dev/null +++ b/b2g/chrome/content/images/errorpage-larry-white.png diff --git a/b2g/chrome/content/images/errorpage-warning.png b/b2g/chrome/content/images/errorpage-warning.png Binary files differnew file mode 100644 index 000000000..8bf9d8e7d --- /dev/null +++ b/b2g/chrome/content/images/errorpage-warning.png diff --git a/b2g/chrome/content/images/exitfullscreen-hdpi.png b/b2g/chrome/content/images/exitfullscreen-hdpi.png Binary files differnew file mode 100644 index 000000000..826e53408 --- /dev/null +++ b/b2g/chrome/content/images/exitfullscreen-hdpi.png diff --git a/b2g/chrome/content/images/fullscreen-hdpi.png b/b2g/chrome/content/images/fullscreen-hdpi.png Binary files differnew file mode 100644 index 000000000..980e78731 --- /dev/null +++ b/b2g/chrome/content/images/fullscreen-hdpi.png diff --git a/b2g/chrome/content/images/mute-hdpi.png b/b2g/chrome/content/images/mute-hdpi.png Binary files differnew file mode 100644 index 000000000..6daf7cf71 --- /dev/null +++ b/b2g/chrome/content/images/mute-hdpi.png diff --git a/b2g/chrome/content/images/pause-hdpi.png b/b2g/chrome/content/images/pause-hdpi.png Binary files differnew file mode 100644 index 000000000..c7837f822 --- /dev/null +++ b/b2g/chrome/content/images/pause-hdpi.png diff --git a/b2g/chrome/content/images/play-hdpi.png b/b2g/chrome/content/images/play-hdpi.png Binary files differnew file mode 100644 index 000000000..fd64f9697 --- /dev/null +++ b/b2g/chrome/content/images/play-hdpi.png diff --git a/b2g/chrome/content/images/scrubber-hdpi.png b/b2g/chrome/content/images/scrubber-hdpi.png Binary files differnew file mode 100644 index 000000000..b965b73d5 --- /dev/null +++ b/b2g/chrome/content/images/scrubber-hdpi.png diff --git a/b2g/chrome/content/images/throbber.png b/b2g/chrome/content/images/throbber.png Binary files differnew file mode 100644 index 000000000..c601ec80b --- /dev/null +++ b/b2g/chrome/content/images/throbber.png diff --git a/b2g/chrome/content/images/unmute-hdpi.png b/b2g/chrome/content/images/unmute-hdpi.png Binary files differnew file mode 100644 index 000000000..5de342bda --- /dev/null +++ b/b2g/chrome/content/images/unmute-hdpi.png diff --git a/b2g/chrome/content/netError.css b/b2g/chrome/content/netError.css new file mode 100644 index 000000000..59d06a00c --- /dev/null +++ b/b2g/chrome/content/netError.css @@ -0,0 +1,131 @@ +/* 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/. */ + +/* + * This defines the look-and-feel styling of the error pages. + * (see: netError.xhtml) + * + * Original styling by William Price <bugzilla@mob.rice.edu> + * Updated for mobile by: Wes Johnston <wjohnston@mozilla.com> + */ + +body { + margin: 0; + padding: 0 8px 8px; + font-family: "Nokia Sans", Tahoma, sans-serif !important; +} + +h1 { + font-size: 22px; +} + +h2 { + font-size: 16px; +} + +ul { + margin: 0px; + padding: 0px 0px 0px 1em; +} + +li { + margin: 0px; + padding: 8px 0px; +} + +#errorPage { + background-color: #CEE6F4; +} + +#errorPage.certerror { + background-color: #EFD400; +} + +#errorPage.blockedsite { + background-color: #BF0000; +} + +#errorTitle { + background: url("chrome://b2g/content/images/errorpage-warning.png") left center no-repeat; + /* Scaled by .666 of their actual size */ + background-size: 40px 40px; + background-origin: content-box; + min-height: 60px; + margin-left: auto; + margin-right: auto; + max-width: 500px; + margin-left: auto; + margin-right: auto; +} + +#errorPage.certerror #errorTitle { + background-image: url("chrome://b2g/content/images/errorpage-larry-black.png"); +} + +#errorPage.blockedsite #errorTitle { + background-image: url("chrome://b2g/content/images/errorpage-larry-white.png"); + color: white; +} + +.errorTitleText { + padding: 0px 0px 0px 50px; + display: inline-block; + vertical-align: middle +} + +#errorPageContainer { + background-color: white; + border: 1px solid #999999; + border-radius: 6px; + padding: 6px 20px 20px; + font-size: 14px; + max-width: 500px; + margin-left: auto; + margin-right: auto; +} + +#errorShortDesc > p:empty { + display: none; +} + +#errorShortDesc > p { + overflow: auto; + border-bottom: 1px solid #999999; + padding-bottom: 1em; +} + +#errorPage.blockedsite #errorShortDesc > p { + font-weight: bold; + border-bottom: none; + padding-bottom: 0px; +} + +#securityOverrideDiv { + padding-top: 10px; +} + +div[collapsed] { + padding-left: 15px; + background-image: url("chrome://b2g/content/images/arrowright-16.png"); + background-size: 11px 11px; + background-repeat: no-repeat; + background-position: left 0.3em; +} + +div[collapsed="true"] { + background-image: url("chrome://b2g/content/images/arrowright-16.png"); +} + +div[collapsed="false"] { + background-image: url("chrome://b2g/content/images/arrowdown-16.png"); +} + +div[collapsed="true"] > p, +div[collapsed="true"] > div { + display: none; +} + +button { + padding: 0.3em !important; +} diff --git a/b2g/chrome/content/screen.js b/b2g/chrome/content/screen.js new file mode 100644 index 000000000..a893e8844 --- /dev/null +++ b/b2g/chrome/content/screen.js @@ -0,0 +1,276 @@ +// screen.js: +// Set the screen size, pixel density and scaling of the b2g client screen +// based on the --screen command-line option, if there is one. +// +// TODO: support multiple device pixels per CSS pixel +// + +var browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); +var isMulet = "ResponsiveUI" in browserWindow; +Cu.import("resource://gre/modules/GlobalSimulatorScreen.jsm"); + +window.addEventListener('ContentStart', onStart); +window.addEventListener('SafeModeStart', onStart); + +// We do this on ContentStart and SafeModeStart because querying the +// displayDPI fails otherwise. +function onStart() { + // This is the toplevel <window> element + let shell = document.getElementById('shell'); + + // The <browser> element inside it + let browser = document.getElementById('systemapp'); + + // Figure out the native resolution of the screen + let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + let hostDPI = windowUtils.displayDPI; + + let DEFAULT_SCREEN = '320x480'; + + // This is a somewhat random selection of named screens. + // Add more to this list when we support more hardware. + // Data from: http://en.wikipedia.org/wiki/List_of_displays_by_pixel_density + let screens = { + iphone: { + name: 'Apple iPhone', width:320, height:480, dpi:163 + }, + ipad: { + name: 'Apple iPad', width:1024, height:768, dpi:132 + }, + nexus_s: { + name: 'Samsung Nexus S', width:480, height:800, dpi:235 + }, + galaxy_s2: { + name: 'Samsung Galaxy SII (I9100)', width:480, height:800, dpi:219 + }, + galaxy_nexus: { + name: 'Samsung Galaxy Nexus', width:720, height:1280, dpi:316 + }, + galaxy_tab: { + name: 'Samsung Galaxy Tab 10.1', width:800, height:1280, dpi:149 + }, + wildfire: { + name: 'HTC Wildfire', width:240, height:320, dpi:125 + }, + tattoo: { + name: 'HTC Tattoo', width:240, height:320, dpi:143 + }, + salsa: { + name: 'HTC Salsa', width:320, height:480, dpi:170 + }, + chacha: { + name: 'HTC ChaCha', width:320, height:480, dpi:222 + }, + }; + + // Get the command line arguments that were passed to the b2g client + let args; + try { + let service = Cc["@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds"].getService(Ci.nsISupports); + args = service.wrappedJSObject.cmdLine; + } catch(e) {} + + let screenarg = null; + + // Get the --screen argument from the command line + try { + if (args) { + screenarg = args.handleFlagWithParam('screen', false); + } + + // Override default screen size with a pref + if (screenarg === null && Services.prefs.prefHasUserValue('b2g.screen.size')) { + screenarg = Services.prefs.getCharPref('b2g.screen.size'); + } + + // If there isn't one, use the default screen + if (screenarg === null) + screenarg = DEFAULT_SCREEN; + + // With no value, tell the user how to use it + if (screenarg == '') + usage(); + } + catch(e) { + // If getting the argument value fails, its an error + usage(); + } + + // Special case --screen=full goes into fullscreen mode + if (screenarg === 'full') { + shell.setAttribute('sizemode', 'fullscreen'); + return; + } + + let width, height, ratio = 1.0; + let lastResizedWidth; + + if (screenarg in screens) { + // If this is a named screen, get its data + let screen = screens[screenarg]; + width = screen.width; + height = screen.height; + ratio = screen.ratio; + } else { + // Otherwise, parse the resolution and density from the --screen value. + // The supported syntax is WIDTHxHEIGHT[@DPI] + let match = screenarg.match(/^(\d+)x(\d+)(@(\d+(\.\d+)?))?$/); + + // Display usage information on syntax errors + if (match == null) + usage(); + + // Convert strings to integers + width = parseInt(match[1], 10); + height = parseInt(match[2], 10); + if (match[4]) + ratio = parseFloat(match[4], 10); + + // If any of the values came out 0 or NaN or undefined, display usage + if (!width || !height || !ratio) { + usage(); + } + } + + Services.prefs.setCharPref('layout.css.devPixelsPerPx', + ratio == 1 ? -1 : ratio); + let defaultOrientation = width < height ? 'portrait' : 'landscape'; + GlobalSimulatorScreen.mozOrientation = GlobalSimulatorScreen.screenOrientation = defaultOrientation; + + function resize() { + GlobalSimulatorScreen.width = width; + GlobalSimulatorScreen.height = height; + + // Set the window width and height to desired size plus chrome + // Include the size of the toolbox displayed under the system app + let controls = document.getElementById('controls'); + let controlsHeight = controls ? controls.getBoundingClientRect().height : 0; + + if (isMulet) { + let tab = browserWindow.gBrowser.selectedTab; + let responsive = ResponsiveUIManager.getResponsiveUIForTab(tab); + responsive.setSize(width + 16*2, + height + controlsHeight + 61); + } else { + let chromewidth = window.outerWidth - window.innerWidth; + let chromeheight = window.outerHeight - window.innerHeight + controlsHeight; + + if (lastResizedWidth == width) { + return; + } + lastResizedWidth = width; + + window.resizeTo(width + chromewidth, + height + chromeheight); + } + + let frameWidth = width, frameHeight = height; + + // If the current app doesn't supports the current screen orientation + // still resize the window, but rotate its frame so that + // it is displayed rotated on the side + let shouldFlip = GlobalSimulatorScreen.mozOrientation != GlobalSimulatorScreen.screenOrientation; + + if (shouldFlip) { + frameWidth = height; + frameHeight = width; + } + + // Set the browser element to the full unscaled size of the screen + let style = browser.style; + style.transform = ''; + style.height = 'calc(100% - ' + controlsHeight + 'px)'; + style.bottom = controlsHeight; + + style.width = frameWidth + "px"; + style.height = frameHeight + "px"; + + if (shouldFlip) { + // Display the system app with a 90° clockwise rotation + let shift = Math.floor(Math.abs(frameWidth - frameHeight) / 2); + style.transform += + ' rotate(0.25turn) translate(-' + shift + 'px, -' + shift + 'px)'; + } + } + + // Resize on startup + resize(); + + // Catch manual resizes to update the internal device size. + window.onresize = function() { + let controls = document.getElementById('controls'); + let controlsHeight = controls ? controls.getBoundingClientRect().height : 0; + + width = window.innerWidth; + height = window.innerHeight - controlsHeight; + + queueResize(); + }; + + // Then resize on each rotation button click, + // or when the system app lock/unlock the orientation + Services.obs.addObserver(function orientationChangeListener(subject) { + let screen = subject.wrappedJSObject; + let { mozOrientation, screenOrientation } = screen; + + // If we have an orientation different than the current one, + // we switch the sizes + if (screenOrientation != defaultOrientation) { + let w = width; + width = height; + height = w; + } + defaultOrientation = screenOrientation; + + queueResize(); + }, 'simulator-adjust-window-size', false); + + // Queue resize request in order to prevent race and slowdowns + // by requesting resize multiple times per loop + let resizeTimeout; + function queueResize() { + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + resizeTimeout = setTimeout(function () { + resizeTimeout = null; + resize(); + }, 0); + } + + // A utility function like console.log() for printing to the terminal window + // Uses dump(), but enables it first, if necessary + function print() { + let dump_enabled = + Services.prefs.getBoolPref('browser.dom.window.dump.enabled'); + + if (!dump_enabled) + Services.prefs.setBoolPref('browser.dom.window.dump.enabled', true); + + dump(Array.prototype.join.call(arguments, ' ') + '\n'); + + if (!dump_enabled) + Services.prefs.setBoolPref('browser.dom.window.dump.enabled', false); + } + + // Print usage info for --screen and exit + function usage() { + // Documentation for the --screen argument + let msg = + 'The --screen argument specifies the desired resolution and\n' + + 'pixel density of the simulated device screen. Use it like this:\n' + + '\t--screen=WIDTHxHEIGHT\t\t\t// E.g.: --screen=320x480\n' + + '\t--screen=WIDTHxHEIGHT@DOTS_PER_INCH\t// E.g.: --screen=480x800@250\n' + + '\t--screen=full\t\t\t\t// run in fullscreen mode\n' + + '\nYou can also specify certain device names:\n'; + for(let p in screens) + msg += '\t--screen=' + p + '\t// ' + screens[p].name + '\n'; + + // Display the usage message + print(msg); + + // Exit the b2g client + Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit); + } +} diff --git a/b2g/chrome/content/settings.js b/b2g/chrome/content/settings.js new file mode 100644 index 000000000..95921da4c --- /dev/null +++ b/b2g/chrome/content/settings.js @@ -0,0 +1,698 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +window.performance.mark('gecko-settings-loadstart'); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +// The load order is important here SettingsRequestManager _must_ be loaded +// prior to using SettingsListener otherwise there is a race in acquiring the +// lock and fulfilling it. If we ever move SettingsListener or this file down in +// the load order of shell.html things will likely break. +Cu.import('resource://gre/modules/SettingsRequestManager.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/AppConstants.jsm'); + +const isGonk = AppConstants.platform === 'gonk'; + +if (isGonk) { + XPCOMUtils.defineLazyGetter(this, "libcutils", function () { + Cu.import("resource://gre/modules/systemlibs.js"); + return libcutils; + }); +} + +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +// Once Bug 731746 - Allow chrome JS object to implement nsIDOMEventTarget +// is resolved this helper could be removed. +var SettingsListener = { + _callbacks: {}, + + init: function sl_init() { + if ('mozSettings' in navigator && navigator.mozSettings) { + navigator.mozSettings.onsettingchange = this.onchange.bind(this); + } + }, + + onchange: function sl_onchange(evt) { + var callback = this._callbacks[evt.settingName]; + if (callback) { + callback(evt.settingValue); + } + }, + + observe: function sl_observe(name, defaultValue, callback) { + var settings = window.navigator.mozSettings; + if (!settings) { + window.setTimeout(function() { callback(defaultValue); }); + return; + } + + if (!callback || typeof callback !== 'function') { + throw new Error('Callback is not a function'); + } + + var req = settings.createLock().get(name); + req.addEventListener('success', (function onsuccess() { + callback(typeof(req.result[name]) != 'undefined' ? + req.result[name] : defaultValue); + })); + + this._callbacks[name] = callback; + } +}; + +SettingsListener.init(); + +// =================== Mono Audio ====================== + +SettingsListener.observe('accessibility.monoaudio.enable', false, function(value) { + Services.prefs.setBoolPref('accessibility.monoaudio.enable', value); +}); + +// =================== Console ====================== + +SettingsListener.observe('debug.console.enabled', true, function(value) { + Services.prefs.setBoolPref('consoleservice.enabled', value); + Services.prefs.setBoolPref('layout.css.report_errors', value); +}); + +SettingsListener.observe('homescreen.manifestURL', 'Sentinel Value' , function(value) { + Services.prefs.setCharPref('dom.mozApps.homescreenURL', value); +}); + +// =================== Languages ==================== +SettingsListener.observe('language.current', 'en-US', function(value) { + Services.prefs.setCharPref('general.useragent.locale', value); + + let prefName = 'intl.accept_languages'; + let defaultBranch = Services.prefs.getDefaultBranch(null); + + let intl = ''; + try { + intl = defaultBranch.getComplexValue(prefName, + Ci.nsIPrefLocalizedString).data; + } catch(e) {} + + // Bug 830782 - Homescreen is in English instead of selected locale after + // the first run experience. + // In order to ensure the current intl value is reflected on the child + // process let's always write a user value, even if this one match the + // current localized pref value. + if (!((new RegExp('^' + value + '[^a-z-_] *[,;]?', 'i')).test(intl))) { + value = value + ', ' + intl; + } else { + value = intl; + } + Services.prefs.setCharPref(prefName, value); + + if (shell.hasStarted() == false) { + shell.bootstrap(); + } +}); + +// =================== RIL ==================== +(function RILSettingsToPrefs() { + // DSDS default service IDs + ['mms', 'sms', 'telephony'].forEach(function(key) { + SettingsListener.observe('ril.' + key + '.defaultServiceId', 0, + function(value) { + if (value != null) { + Services.prefs.setIntPref('dom.' + key + '.defaultServiceId', value); + } + }); + }); +})(); + +//=================== DeviceInfo ==================== +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +Components.utils.import('resource://gre/modules/ctypes.jsm'); +(function DeviceInfoToSettings() { + // MOZ_B2G_VERSION is set in b2g/confvars.sh, and is output as a #define value + // from configure.in, defaults to 1.0.0 if this value is not exist. + let os_version = AppConstants.MOZ_B2G_VERSION; + let os_name = AppConstants.MOZ_B2G_OS_NAME; + + let appInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); + + // Get the hardware info and firmware revision from device properties. + let hardware_info = null; + let firmware_revision = null; + let product_manufacturer = null; + let product_model = null; + let product_device = null; + let build_number = null; + if (isGonk) { + hardware_info = libcutils.property_get('ro.hardware'); + firmware_revision = libcutils.property_get('ro.firmware_revision'); + product_manufacturer = libcutils.property_get('ro.product.manufacturer'); + product_model = libcutils.property_get('ro.product.model'); + product_device = libcutils.property_get('ro.product.device'); + build_number = libcutils.property_get('ro.build.version.incremental'); + } + + // Populate deviceinfo settings, + // copying any existing deviceinfo.os into deviceinfo.previous_os + let lock = window.navigator.mozSettings.createLock(); + let req = lock.get('deviceinfo.os'); + req.onsuccess = req.onerror = () => { + let previous_os = req.result && req.result['deviceinfo.os'] || ''; + let software = os_name + ' ' + os_version; + let setting = { + 'deviceinfo.build_number': build_number, + 'deviceinfo.os': os_version, + 'deviceinfo.previous_os': previous_os, + 'deviceinfo.software': software, + 'deviceinfo.platform_version': appInfo.platformVersion, + 'deviceinfo.platform_build_id': appInfo.platformBuildID, + 'deviceinfo.hardware': hardware_info, + 'deviceinfo.firmware_revision': firmware_revision, + 'deviceinfo.product_manufacturer': product_manufacturer, + 'deviceinfo.product_model': product_model, + 'deviceinfo.product_device': product_device + } + lock.set(setting); + } +})(); + +// =================== DevTools ==================== + +var developerHUD; +SettingsListener.observe('devtools.overlay', false, (value) => { + if (value) { + if (!developerHUD) { + let scope = {}; + Services.scriptloader.loadSubScript('chrome://b2g/content/devtools/hud.js', scope); + developerHUD = scope.developerHUD; + } + developerHUD.init(); + } else { + if (developerHUD) { + developerHUD.uninit(); + } + } +}); + +if (isGonk) { + var LogShake; + (function() { + let scope = {}; + Cu.import('resource://gre/modules/LogShake.jsm', scope); + LogShake = scope.LogShake; + LogShake.init(); + })(); + + SettingsListener.observe('devtools.logshake.enabled', false, value => { + if (value) { + LogShake.enableDeviceMotionListener(); + } else { + LogShake.disableDeviceMotionListener(); + } + }); + + SettingsListener.observe('devtools.logshake.qa_enabled', false, value => { + if (value) { + LogShake.enableQAMode(); + } else { + LogShake.disableQAMode(); + } + }); +} + +// =================== Device Storage ==================== +SettingsListener.observe('device.storage.writable.name', 'sdcard', function(value) { + if (Services.prefs.getPrefType('device.storage.writable.name') != Ci.nsIPrefBranch.PREF_STRING) { + // We clear the pref because it used to be erroneously written as a bool + // and we need to clear it before we can change it to have the correct type. + Services.prefs.clearUserPref('device.storage.writable.name'); + } + Services.prefs.setCharPref('device.storage.writable.name', value); +}); + +// =================== Privacy ==================== +SettingsListener.observe('privacy.donottrackheader.value', 1, function(value) { + Services.prefs.setIntPref('privacy.donottrackheader.value', value); + // If the user specifically disallows tracking, we set the value of + // app.update.custom (update tracking ID) to an empty string. + if (value == 1) { + Services.prefs.setCharPref('app.update.custom', ''); + return; + } + // Otherwise, we assure that the update tracking ID exists. + setUpdateTrackingId(); +}); + +// =================== Crash Reporting ==================== +SettingsListener.observe('app.reportCrashes', 'ask', function(value) { + if (value == 'always') { + Services.prefs.setBoolPref('app.reportCrashes', true); + } else if (value == 'never') { + Services.prefs.setBoolPref('app.reportCrashes', false); + } else { + Services.prefs.clearUserPref('app.reportCrashes'); + } + // This preference is consulted during startup. + Services.prefs.savePrefFile(null); +}); + +// ================ Updates ================ +/** + * For tracking purposes some partners require us to add an UUID to the + * update URL. The update tracking ID will be an empty string if the + * do-not-track feature specifically disallows tracking and it is reseted + * to a different ID if the do-not-track value changes from disallow to allow. + */ +function setUpdateTrackingId() { + try { + let dntEnabled = Services.prefs.getBoolPref('privacy.donottrackheader.enabled'); + let dntValue = Services.prefs.getIntPref('privacy.donottrackheader.value'); + // If the user specifically decides to disallow tracking (1), we just bail out. + if (dntEnabled && (dntValue == 1)) { + return; + } + + let trackingId = + Services.prefs.getPrefType('app.update.custom') == + Ci.nsIPrefBranch.PREF_STRING && + Services.prefs.getCharPref('app.update.custom'); + + // If there is no previous registered tracking ID, we generate a new one. + // This should only happen on first usage or after changing the + // do-not-track value from disallow to allow. + if (!trackingId) { + trackingId = uuidgen.generateUUID().toString().replace(/[{}]/g, ""); + Services.prefs.setCharPref('app.update.custom', trackingId); + } + } catch(e) { + dump('Error getting tracking ID ' + e + '\n'); + } +} +setUpdateTrackingId(); + +(function syncUpdatePrefs() { + // The update service reads the prefs from the default branch. This is by + // design, as explained in bug 302721 comment 43. If we are to successfully + // modify them, that's where we need to make our changes. + let defaultBranch = Services.prefs.getDefaultBranch(null); + + function syncPrefDefault(prefName) { + // The pref value at boot-time will serve as default for the setting. + let defaultValue = defaultBranch.getCharPref(prefName); + let defaultSetting = {}; + defaultSetting[prefName] = defaultValue; + + // We back up that value in order to detect pref changes across reboots. + // Such a change can happen e.g. when the user installs an OTA update that + // changes the update URL format. + let backupName = prefName + '.old'; + try { + // Everything relies on the comparison below: When pushing a new Gecko + // that changes app.update.url or app.update.channel, we overwrite any + // existing setting with the new pref value. + let backupValue = Services.prefs.getCharPref(backupName); + if (defaultValue !== backupValue) { + // If the pref has changed since our last backup, overwrite the setting. + navigator.mozSettings.createLock().set(defaultSetting); + } + } catch(e) { + // There was no backup: Overwrite the setting and create a backup below. + navigator.mozSettings.createLock().set(defaultSetting); + } + + // Initialize or update the backup value. + Services.prefs.setCharPref(backupName, defaultValue); + + // Propagate setting changes to the pref. + SettingsListener.observe(prefName, defaultValue, value => { + if (!value) { + // If the setting value is invalid, reset it to its default. + navigator.mozSettings.createLock().set(defaultSetting); + return; + } + // Here we will overwrite the pref with the setting value. + defaultBranch.setCharPref(prefName, value); + }); + } + + syncPrefDefault('app.update.url'); + syncPrefDefault('app.update.channel'); +})(); + +// ================ Debug ================ +(function Composer2DSettingToPref() { + //layers.composer.enabled can be enabled in three ways + //In order of precedence they are: + // + //1. mozSettings "layers.composer.enabled" + //2. a gecko pref "layers.composer.enabled" + //3. presence of ro.display.colorfill at the Gonk level + + var req = navigator.mozSettings.createLock().get('layers.composer2d.enabled'); + req.onsuccess = function() { + if (typeof(req.result['layers.composer2d.enabled']) === 'undefined') { + var enabled = false; + if (Services.prefs.getPrefType('layers.composer2d.enabled') == Ci.nsIPrefBranch.PREF_BOOL) { + enabled = Services.prefs.getBoolPref('layers.composer2d.enabled'); + } else if (isGonk) { + let androidVersion = libcutils.property_get("ro.build.version.sdk"); + if (androidVersion >= 17 ) { + enabled = true; + } else { + enabled = (libcutils.property_get('ro.display.colorfill') === '1'); + } + } + navigator.mozSettings.createLock().set({'layers.composer2d.enabled': enabled }); + } + + SettingsListener.observe("layers.composer2d.enabled", true, function(value) { + Services.prefs.setBoolPref("layers.composer2d.enabled", value); + }); + }; + req.onerror = function() { + dump("Error configuring layers.composer2d.enabled setting"); + }; + +})(); + +// ================ Accessibility ============ +(function setupAccessibility() { + let accessibilityScope = {}; + SettingsListener.observe("accessibility.screenreader", false, function(value) { + if (!value) { + return; + } + if (!('AccessFu' in accessibilityScope)) { + Cu.import('resource://gre/modules/accessibility/AccessFu.jsm', + accessibilityScope); + accessibilityScope.AccessFu.attach(window); + } + }); +})(); + +// ================ Theming ============ +(function themingSettingsListener() { + let themingPrefs = ['ui.menu', 'ui.menutext', 'ui.infobackground', 'ui.infotext', + 'ui.window', 'ui.windowtext', 'ui.highlight']; + + themingPrefs.forEach(function(pref) { + SettingsListener.observe('gaia.' + pref, null, function(value) { + if (value) { + Services.prefs.setCharPref(pref, value); + } + }); + }); +})(); + +// =================== Telemetry ====================== +(function setupTelemetrySettings() { + let gaiaSettingName = 'debug.performance_data.shared'; + let geckoPrefName = 'toolkit.telemetry.enabled'; + SettingsListener.observe(gaiaSettingName, null, function(value) { + if (value !== null) { + // Gaia setting has been set; update Gecko pref to that. + Services.prefs.setBoolPref(geckoPrefName, value); + return; + } + // Gaia setting has not been set; set the gaia setting to default. + let prefValue = AppConstants.MOZ_TELEMETRY_ON_BY_DEFAULT; + try { + prefValue = Services.prefs.getBoolPref(geckoPrefName); + } catch (e) { + // Pref not set; use default value. + } + let setting = {}; + setting[gaiaSettingName] = prefValue; + window.navigator.mozSettings.createLock().set(setting); + }); +})(); + +// =================== Low-precision buffer ====================== +(function setupLowPrecisionSettings() { + // The gaia setting layers.low-precision maps to two gecko prefs + SettingsListener.observe('layers.low-precision', null, function(value) { + if (value !== null) { + // Update gecko from the new Gaia setting + Services.prefs.setBoolPref('layers.low-precision-buffer', value); + Services.prefs.setBoolPref('layers.progressive-paint', value); + } else { + // Update gaia setting from gecko value + try { + let prefValue = Services.prefs.getBoolPref('layers.low-precision-buffer'); + let setting = { 'layers.low-precision': prefValue }; + window.navigator.mozSettings.createLock().set(setting); + } catch (e) { + console.log('Unable to read pref layers.low-precision-buffer: ' + e); + } + } + }); + + // The gaia setting layers.low-opacity maps to a string gecko pref (0.5/1.0) + SettingsListener.observe('layers.low-opacity', null, function(value) { + if (value !== null) { + // Update gecko from the new Gaia setting + Services.prefs.setCharPref('layers.low-precision-opacity', value ? '0.5' : '1.0'); + } else { + // Update gaia setting from gecko value + try { + let prefValue = Services.prefs.getCharPref('layers.low-precision-opacity'); + let setting = { 'layers.low-opacity': (prefValue == '0.5') }; + window.navigator.mozSettings.createLock().set(setting); + } catch (e) { + console.log('Unable to read pref layers.low-precision-opacity: ' + e); + } + } + }); +})(); + +// ======================= Dogfooders FOTA ========================== +if (AppConstants.MOZ_B2G_RIL) { + XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils", + "resource://gre/modules/AppsUtils.jsm"); + + SettingsListener.observe('debug.performance_data.dogfooding', false, + isDogfooder => { + if (!isDogfooder) { + dump('AUS:Settings: Not a dogfooder!\n'); + return; + } + + if (!('mozTelephony' in navigator)) { + dump('AUS:Settings: There is no mozTelephony!\n'); + return; + } + + if (!('mozMobileConnections' in navigator)) { + dump('AUS:Settings: There is no mozMobileConnections!\n'); + return; + } + + let conn = navigator.mozMobileConnections[0]; + conn.addEventListener('radiostatechange', function onradiostatechange() { + if (conn.radioState !== 'enabled') { + return; + } + + conn.removeEventListener('radiostatechange', onradiostatechange); + navigator.mozTelephony.dial('*#06#').then(call => { + return call.result.then(res => { + if (res.success && res.statusMessage + && (res.serviceCode === 'scImei')) { + Services.prefs.setCharPref("app.update.imei_hash", + AppsUtils.computeHash(res.statusMessage, "SHA512")); + } + }); + }); + }); + }); +} + +// =================== Various simple mapping ====================== +var settingsToObserve = { + 'accessibility.screenreader_quicknav_modes': { + prefName: 'accessibility.accessfu.quicknav_modes', + resetToPref: true, + defaultValue: '' + }, + 'accessibility.screenreader_quicknav_index': { + prefName: 'accessibility.accessfu.quicknav_index', + resetToPref: true, + defaultValue: 0 + }, + 'app.update.interval': 86400, + 'apz.overscroll.enabled': true, + 'browser.safebrowsing.phishing.enabled': true, + 'browser.safebrowsing.malware.enabled': true, + 'debug.fps.enabled': { + prefName: 'layers.acceleration.draw-fps', + defaultValue: false + }, + 'debug.log-animations.enabled': { + prefName: 'layers.offmainthreadcomposition.log-animations', + defaultValue: false + }, + 'debug.paint-flashing.enabled': { + prefName: 'nglayout.debug.paint_flashing', + defaultValue: false + }, + // FIXME: Bug 1185806 - Provide a common device name setting. + // Borrow device name from developer's menu to avoid multiple name settings. + 'devtools.discovery.device': { + prefName: 'dom.presentation.device.name', + defaultValue: 'Firefox OS' + }, + 'devtools.eventlooplag.threshold': 100, + 'devtools.remote.wifi.visible': { + resetToPref: true + }, + 'devtools.telemetry.supported_performance_marks': { + resetToPref: true + }, + + 'dom.presentation.discovery.enabled': false, + 'dom.presentation.discoverable': false, + 'dom.serviceWorkers.testing.enabled': false, + 'gfx.layerscope.enabled': false, + 'layers.draw-borders': false, + 'layers.draw-tile-borders': false, + 'layers.dump': false, + 'layers.enable-tiles': AppConstants.platform !== "win", + 'layers.enable-tiles': true, + 'layers.effect.invert': false, + 'layers.effect.grayscale': false, + 'layers.effect.contrast': '0.0', + 'layout.display-list.dump': false, + 'mms.debugging.enabled': false, + 'network.debugging.enabled': false, + 'privacy.donottrackheader.enabled': false, + 'privacy.trackingprotection.enabled': false, + 'ril.debugging.enabled': false, + 'ril.radio.disabled': false, + 'ril.mms.requestReadReport.enabled': { + prefName: 'dom.mms.requestReadReport', + defaultValue: true + }, + 'ril.mms.requestStatusReport.enabled': { + prefName: 'dom.mms.requestStatusReport', + defaultValue: false + }, + 'ril.mms.retrieval_mode': { + prefName: 'dom.mms.retrieval_mode', + defaultValue: 'manual' + }, + 'ril.sms.requestStatusReport.enabled': { + prefName: 'dom.sms.requestStatusReport', + defaultValue: false + }, + 'ril.sms.strict7BitEncoding.enabled': { + prefName: 'dom.sms.strict7BitEncoding', + defaultValue: false + }, + 'ril.sms.maxReadAheadEntries': { + prefName: 'dom.sms.maxReadAheadEntries', + defaultValue: 7 + }, + 'services.sync.enabled': { + defaultValue: false, + notifyChange: true + }, + 'ui.touch.radius.leftmm': { + resetToPref: true + }, + 'ui.touch.radius.topmm': { + resetToPref: true + }, + 'ui.touch.radius.rightmm': { + resetToPref: true + }, + 'ui.touch.radius.bottommm': { + resetToPref: true + }, + 'ui.click_hold_context_menus.delay': { + resetToPref: true + }, + 'wap.UAProf.tagname': 'x-wap-profile', + 'wap.UAProf.url': '' +}; + +if (AppConstants.MOZ_GRAPHENE) { + // Restart required + settingsToObserve['layers.async-pan-zoom.enabled'] = false; +} + +function settingObserver(setPref, prefName, setting) { + return value => { + setPref(prefName, value); + if (setting.notifyChange) { + SystemAppProxy._sendCustomEvent('mozPrefChromeEvent', { + prefName: prefName, + value: value + }); + } + }; +} + +for (let key in settingsToObserve) { + let setting = settingsToObserve[key]; + + // Allow setting to contain flags redefining prefName and defaultValue. + let prefName = setting.prefName || key; + let defaultValue = setting.defaultValue; + if (defaultValue === undefined) { + defaultValue = setting; + } + + let prefs = Services.prefs; + + // If requested, reset setting value and defaultValue to the pref value. + if (setting.resetToPref) { + switch (prefs.getPrefType(prefName)) { + case Ci.nsIPrefBranch.PREF_BOOL: + defaultValue = prefs.getBoolPref(prefName); + break; + + case Ci.nsIPrefBranch.PREF_INT: + defaultValue = prefs.getIntPref(prefName); + break; + + case Ci.nsIPrefBranch.PREF_STRING: + defaultValue = prefs.getCharPref(prefName); + break; + } + + let setting = {}; + setting[key] = defaultValue; + window.navigator.mozSettings.createLock().set(setting); + } + + // Figure out the right setter function for this type of pref. + let setPref; + switch (typeof defaultValue) { + case 'boolean': + setPref = prefs.setBoolPref.bind(prefs); + break; + + case 'number': + setPref = prefs.setIntPref.bind(prefs); + break; + + case 'string': + setPref = prefs.setCharPref.bind(prefs); + break; + } + + SettingsListener.observe(key, defaultValue, + settingObserver(setPref, prefName, setting)); +}; diff --git a/b2g/chrome/content/shell.css b/b2g/chrome/content/shell.css new file mode 100644 index 000000000..34daafd99 --- /dev/null +++ b/b2g/chrome/content/shell.css @@ -0,0 +1,81 @@ +/* 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/. */ + +html { + background: black; + overflow: hidden; + width: 100%; + height: 100%; + padding: 0 !important; +} +body { + margin: 0; + width: 100%; + height: 100%; + overflow: hidden; +} +iframe { + overflow: hidden; + height: 100%; + width: 100%; + border: none; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 1; + -moz-user-select: none; +} + +%ifdef MOZ_GRAPHENE + +body.content-loaded > #installing { + display: none; +} + +#installing { + z-index: 2; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: #F1C40F; + color: #FFF; +} + +.throbber { + width: 3px; + height: 3px; + border-radius: 100px; + background-color: #FFF; + animation-name: throbber; + animation-duration: 1500ms; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +#titlebar-buttonbox { + margin: 6px 7px; + -moz-appearance: -moz-window-button-box; +} + +@keyframes throbber{ + from { + transform: scale(0); + opacity: 0.4; + } + to { + transform: scale(400); + opacity: 0; + } +} + +%endif diff --git a/b2g/chrome/content/shell.html b/b2g/chrome/content/shell.html new file mode 100644 index 000000000..5507a65aa --- /dev/null +++ b/b2g/chrome/content/shell.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml" + id="shell" + windowtype="navigator:browser" +#ifdef ANDROID + sizemode="fullscreen" +#endif +#ifdef MOZ_GRAPHENE + macanimationtype="document" + fullscreenbutton="true" + chromemargin="0,0,0,0" +#endif + > + +<head> + <link rel="stylesheet" href="shell.css" type="text/css"> + <script type="text/javascript"> + <!-- Add raptor performance marker --> + window.performance.mark('gecko-shell-html-load'); + </script> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/settings.js"> </script> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/shell.js"> </script> + +#ifndef ANDROID +#ifndef MOZ_GRAPHENE + <!-- various task that has to happen only on desktop --> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/desktop.js"> </script> + <!-- this script handles the screen argument for desktop builds --> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/screen.js"> </script> +#endif +#else + <!-- this file is only loaded on Gonk to manage ADB state --> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/devtools/adb.js"> </script> +#endif + <!-- manages DevTools server state --> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/devtools/debugger.js"> </script> +</head> + <body id="container"> +#ifndef MOZ_GRAPHENE +#ifdef MOZ_WIDGET_COCOA + <!-- + If the document is empty at startup, we don't display the window + at all on Mac OS... + --> + <h1 id="placeholder">wtf mac os!</h1> +#endif +#else + <div id="titlebar-buttonbox"></div> + <div id="installing"> + <div class="throbber"></div> + <div class="message"></div> + </div> +#endif + <!-- The html:iframe containing the UI is created here. --> + </body> +</html> diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js new file mode 100644 index 000000000..d483f9a64 --- /dev/null +++ b/b2g/chrome/content/shell.js @@ -0,0 +1,1308 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +window.performance.mark('gecko-shell-loadstart'); + +Cu.import('resource://gre/modules/NotificationDB.jsm'); +Cu.import("resource://gre/modules/AppsUtils.jsm"); +Cu.import('resource://gre/modules/UserAgentOverrides.jsm'); +Cu.import('resource://gre/modules/Keyboard.jsm'); +Cu.import('resource://gre/modules/ErrorPage.jsm'); +Cu.import('resource://gre/modules/AlertsHelper.jsm'); +Cu.import('resource://gre/modules/SystemUpdateService.jsm'); + +if (isGonk) { + Cu.import('resource://gre/modules/NetworkStatsService.jsm'); + Cu.import('resource://gre/modules/ResourceStatsService.jsm'); +} + +// Identity +Cu.import('resource://gre/modules/SignInToWebsite.jsm'); +SignInToWebsiteController.init(); + +Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm'); +Cu.import('resource://gre/modules/DownloadsAPI.jsm'); +Cu.import('resource://gre/modules/PresentationDeviceInfoManager.jsm'); +Cu.import('resource://gre/modules/AboutServiceWorkers.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Screenshot", + "resource://gre/modules/Screenshot.jsm"); + +XPCOMUtils.defineLazyServiceGetter(Services, 'env', + '@mozilla.org/process/environment;1', + 'nsIEnvironment'); + +XPCOMUtils.defineLazyServiceGetter(Services, 'ss', + '@mozilla.org/content/style-sheet-service;1', + 'nsIStyleSheetService'); + +XPCOMUtils.defineLazyServiceGetter(this, 'gSystemMessenger', + '@mozilla.org/system-message-internal;1', + 'nsISystemMessagesInternal'); + +XPCOMUtils.defineLazyGetter(this, "ppmm", function() { + return Cc["@mozilla.org/parentprocessmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); +}); + +if (isGonk) { + XPCOMUtils.defineLazyGetter(this, "libcutils", function () { + Cu.import("resource://gre/modules/systemlibs.js"); + return libcutils; + }); +} + +XPCOMUtils.defineLazyServiceGetter(Services, 'captivePortalDetector', + '@mozilla.org/toolkit/captive-detector;1', + 'nsICaptivePortalDetector'); + +XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", + "resource://gre/modules/SafeBrowsing.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SafeMode", + "resource://gre/modules/SafeMode.jsm"); + +window.performance.measure('gecko-shell-jsm-loaded', 'gecko-shell-loadstart'); + +function debug(str) { + dump(' -*- Shell.js: ' + str + '\n'); +} + +const once = event => { + let target = shell.contentBrowser; + return new Promise((resolve, reject) => { + target.addEventListener(event, function gotEvent(evt) { + target.removeEventListener(event, gotEvent, false); + resolve(evt); + }, false); + }); +} + +function clearCache() { + let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Ci.nsICacheStorageService); + cache.clear(); +} + +function clearCacheAndReload() { + // Reload the main frame with a cleared cache. + debug('Reloading ' + shell.contentBrowser.contentWindow.location); + clearCache(); + shell.contentBrowser.contentWindow.location.reload(true); + once('mozbrowserlocationchange').then( + evt => { + shell.sendEvent(window, "ContentStart"); + }); +} + +function restart() { + let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit | Ci.nsIAppStartup.eRestart); +} + +function debugCrashReport(aStr) { + AppConstants.MOZ_CRASHREPORTER && dump('Crash reporter : ' + aStr); +} + +var shell = { + + get CrashSubmit() { + delete this.CrashSubmit; + if (AppConstants.MOZ_CRASHREPORTER) { + Cu.import("resource://gre/modules/CrashSubmit.jsm", this); + return this.CrashSubmit; + } else { + dump('Crash reporter : disabled at build time.'); + return this.CrashSubmit = null; + } + }, + + onlineForCrashReport: function shell_onlineForCrashReport() { + let wifiManager = navigator.mozWifiManager; + let onWifi = (wifiManager && + (wifiManager.connection.status == 'connected')); + return !Services.io.offline && onWifi; + }, + + reportCrash: function shell_reportCrash(isChrome, aCrashID) { + let crashID = aCrashID; + try { + // For chrome crashes, we want to report the lastRunCrashID. + if (isChrome) { + crashID = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime).lastRunCrashID; + } + } catch(e) { + debugCrashReport('Failed to fetch crash id. Crash ID is "' + crashID + + '" Exception: ' + e); + } + + // Bail if there isn't a valid crashID. + if (!this.CrashSubmit || !crashID && !this.CrashSubmit.pendingIDs().length) { + return; + } + + // purge the queue. + this.CrashSubmit.pruneSavedDumps(); + + // check for environment affecting crash reporting + let env = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + let shutdown = env.get("MOZ_CRASHREPORTER_SHUTDOWN"); + if (shutdown) { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); + } + + let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT"); + if (noReport) { + return; + } + + try { + // Check if we should automatically submit this crash. + if (Services.prefs.getBoolPref('app.reportCrashes')) { + this.submitCrash(crashID); + } else { + this.deleteCrash(crashID); + } + } catch (e) { + debugCrashReport('Can\'t fetch app.reportCrashes. Exception: ' + e); + } + + // We can get here if we're just submitting old pending crashes. + // Check that there's a valid crashID so that we only notify the + // user if a crash just happened and not when we OOM. Bug 829477 + if (crashID) { + this.sendChromeEvent({ + type: "handle-crash", + crashID: crashID, + chrome: isChrome + }); + } + }, + + deleteCrash: function shell_deleteCrash(aCrashID) { + if (aCrashID) { + debugCrashReport('Deleting pending crash: ' + aCrashID); + shell.CrashSubmit.delete(aCrashID); + } + }, + + // this function submit the pending crashes. + // make sure you are online. + submitQueuedCrashes: function shell_submitQueuedCrashes() { + // submit the pending queue. + let pending = shell.CrashSubmit.pendingIDs(); + for (let crashid of pending) { + debugCrashReport('Submitting crash: ' + crashid); + shell.CrashSubmit.submit(crashid); + } + }, + + // This function submits a crash when we're online. + submitCrash: function shell_submitCrash(aCrashID) { + if (this.onlineForCrashReport()) { + this.submitQueuedCrashes(); + return; + } + + debugCrashReport('Not online, postponing.'); + + Services.obs.addObserver(function observer(subject, topic, state) { + let network = subject.QueryInterface(Ci.nsINetworkInfo); + if (network.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED + && network.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI) { + shell.submitQueuedCrashes(); + + Services.obs.removeObserver(observer, topic); + } + }, "network-connection-state-changed", false); + }, + + get homeURL() { + try { + let homeSrc = Services.env.get('B2G_HOMESCREEN'); + if (homeSrc) + return homeSrc; + } catch (e) {} + + return Services.prefs.getCharPref('b2g.system_startup_url'); + }, + + get manifestURL() { + return Services.prefs.getCharPref('b2g.system_manifest_url'); + }, + + _started: false, + hasStarted: function shell_hasStarted() { + return this._started; + }, + + bootstrap: function() { + window.performance.mark('gecko-shell-bootstrap'); + + // Before anything, check if we want to start in safe mode. + SafeMode.check(window).then(() => { + let startManifestURL = + Cc['@mozilla.org/commandlinehandler/general-startup;1?type=b2gbootstrap'] + .getService(Ci.nsISupports).wrappedJSObject.startManifestURL; + + // If --start-manifest hasn't been specified, we re-use the latest specified manifest. + // If it's the first launch, we will fallback to b2g.default.start_manifest_url + if (AppConstants.MOZ_GRAPHENE && !startManifestURL) { + try { + startManifestURL = Services.prefs.getCharPref("b2g.system_manifest_url"); + } catch(e) {} + } + + if (!startManifestURL) { + try { + startManifestURL = Services.prefs.getCharPref("b2g.default.start_manifest_url"); + } catch(e) {} + } + + if (startManifestURL) { + Cu.import('resource://gre/modules/Bootstraper.jsm'); + + if (AppConstants.MOZ_GRAPHENE && Bootstraper.isInstallRequired(startManifestURL)) { + // Installing the app my take some time. We don't want to keep the + // native window hidden. + showInstallScreen(); + } + + Bootstraper.ensureSystemAppInstall(startManifestURL) + .then(this.start.bind(this)) + .catch(Bootstraper.bailout); + } else { + this.start(); + } + }); + }, + + start: function shell_start() { + window.performance.mark('gecko-shell-start'); + this._started = true; + + // This forces the initialization of the cookie service before we hit the + // network. + // See bug 810209 + let cookies = Cc["@mozilla.org/cookieService;1"]; + + try { + let cr = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsICrashReporter); + // Dogfood id. We might want to remove it in the future. + // see bug 789466 + try { + let dogfoodId = Services.prefs.getCharPref('prerelease.dogfood.id'); + if (dogfoodId != "") { + cr.annotateCrashReport("Email", dogfoodId); + } + } + catch (e) { } + + if (isGonk) { + // Annotate crash report + let annotations = [ [ "Android_Hardware", "ro.hardware" ], + [ "Android_Device", "ro.product.device" ], + [ "Android_CPU_ABI2", "ro.product.cpu.abi2" ], + [ "Android_CPU_ABI", "ro.product.cpu.abi" ], + [ "Android_Manufacturer", "ro.product.manufacturer" ], + [ "Android_Brand", "ro.product.brand" ], + [ "Android_Model", "ro.product.model" ], + [ "Android_Board", "ro.product.board" ], + ]; + + annotations.forEach(function (element) { + cr.annotateCrashReport(element[0], libcutils.property_get(element[1])); + }); + + let androidVersion = libcutils.property_get("ro.build.version.sdk") + + "(" + libcutils.property_get("ro.build.version.codename") + ")"; + cr.annotateCrashReport("Android_Version", androidVersion); + + SettingsListener.observe("deviceinfo.os", "", function(value) { + try { + let cr = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsICrashReporter); + cr.annotateCrashReport("B2G_OS_Version", value); + } catch(e) { } + }); + } + } catch(e) { + debugCrashReport('exception: ' + e); + } + + let homeURL = this.homeURL; + if (!homeURL) { + let msg = 'Fatal error during startup: No homescreen found: try setting B2G_HOMESCREEN'; + alert(msg); + return; + } + + let manifestURL = this.manifestURL; + // <html:iframe id="systemapp" + // mozbrowser="true" allowfullscreen="true" + // style="overflow: hidden; height: 100%; width: 100%; border: none;" + // src="data:text/html;charset=utf-8,%3C!DOCTYPE html>%3Cbody style='background:black;'>"/> + let systemAppFrame = + document.createElementNS('http://www.w3.org/1999/xhtml', 'html:iframe'); + systemAppFrame.setAttribute('id', 'systemapp'); + systemAppFrame.setAttribute('mozbrowser', 'true'); + systemAppFrame.setAttribute('mozapp', manifestURL); + systemAppFrame.setAttribute('allowfullscreen', 'true'); + systemAppFrame.setAttribute('src', 'blank.html'); + let container = document.getElementById('container'); + + if (AppConstants.platform == 'macosx') { + // See shell.html + let hotfix = document.getElementById('placeholder'); + if (hotfix) { + container.removeChild(hotfix); + } + } + + this.contentBrowser = container.appendChild(systemAppFrame); + + let webNav = systemAppFrame.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + webNav.sessionHistory = Cc["@mozilla.org/browser/shistory;1"].createInstance(Ci.nsISHistory); + + if (AppConstants.MOZ_GRAPHENE) { + webNav.QueryInterface(Ci.nsIDocShell).windowDraggingAllowed = true; + } + + let audioChannels = systemAppFrame.allowedAudioChannels; + audioChannels && audioChannels.forEach(function(audioChannel) { + // Set all audio channels as unmuted by default + // because some audio in System app will be played + // before AudioChannelService[1] is Gaia is loaded. + // [1]: https://github.com/mozilla-b2g/gaia/blob/master/apps/system/js/audio_channel_service.js + audioChannel.setMuted(false); + }); + + // On firefox mulet, shell.html is loaded in a tab + // and we have to listen on the chrome event handler + // to catch key events + let chromeEventHandler = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler || window; + // Capture all key events so we can filter out hardware buttons + // And send them to Gaia via mozChromeEvents. + // Ideally, hardware buttons wouldn't generate key events at all, or + // if they did, they would use keycodes that conform to DOM 3 Events. + // See discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=762362 + chromeEventHandler.addEventListener('keydown', this, true); + chromeEventHandler.addEventListener('keyup', this, true); + + window.addEventListener('MozApplicationManifest', this); + window.addEventListener('MozAfterPaint', this); + window.addEventListener('sizemodechange', this); + window.addEventListener('unload', this); + this.contentBrowser.addEventListener('mozbrowserloadstart', this, true); + this.contentBrowser.addEventListener('mozbrowserscrollviewchange', this, true); + this.contentBrowser.addEventListener('mozbrowsercaretstatechanged', this); + + CustomEventManager.init(); + UserAgentOverrides.init(); + CaptivePortalLoginHelper.init(); + + this.contentBrowser.src = homeURL; + + this._isEventListenerReady = false; + + window.performance.mark('gecko-shell-system-frame-set'); + + ppmm.addMessageListener("content-handler", this); + ppmm.addMessageListener("dial-handler", this); + ppmm.addMessageListener("sms-handler", this); + ppmm.addMessageListener("mail-handler", this); + ppmm.addMessageListener("file-picker", this); + + setTimeout(function() { + SafeBrowsing.init(); + }, 5000); + }, + + stop: function shell_stop() { + window.removeEventListener('unload', this); + window.removeEventListener('keydown', this, true); + window.removeEventListener('keyup', this, true); + window.removeEventListener('MozApplicationManifest', this); + window.removeEventListener('sizemodechange', this); + this.contentBrowser.removeEventListener('mozbrowserloadstart', this, true); + this.contentBrowser.removeEventListener('mozbrowserscrollviewchange', this, true); + this.contentBrowser.removeEventListener('mozbrowsercaretstatechanged', this); + ppmm.removeMessageListener("content-handler", this); + + UserAgentOverrides.uninit(); + }, + + // If this key event represents a hardware button which needs to be send as + // a message, broadcasts it with the message set to 'xxx-button-press' or + // 'xxx-button-release'. + broadcastHardwareKeys: function shell_broadcastHardwareKeys(evt) { + let type; + let message; + + let mediaKeys = { + 'MediaTrackNext': 'media-next-track-button', + 'MediaTrackPrevious': 'media-previous-track-button', + 'MediaPause': 'media-pause-button', + 'MediaPlay': 'media-play-button', + 'MediaPlayPause': 'media-play-pause-button', + 'MediaStop': 'media-stop-button', + 'MediaRewind': 'media-rewind-button', + 'MediaFastForward': 'media-fast-forward-button' + }; + + if (evt.keyCode == evt.DOM_VK_F1) { + type = 'headset-button'; + message = 'headset-button'; + } else if (mediaKeys[evt.key]) { + type = 'media-button'; + message = mediaKeys[evt.key]; + } else { + return; + } + + switch (evt.type) { + case 'keydown': + message = message + '-press'; + break; + case 'keyup': + message = message + '-release'; + break; + } + + // Let applications receive the headset button and media key press/release message. + if (message !== this.lastHardwareButtonMessage) { + this.lastHardwareButtonMessage = message; + gSystemMessenger.broadcastMessage(type, message); + } + }, + + lastHardwareButtonMessage: null, // property for the hack above + visibleNormalAudioActive: false, + + handleEvent: function shell_handleEvent(evt) { + function checkReloadKey() { + if (evt.type !== 'keyup') { + return false; + } + + try { + let key = JSON.parse(Services.prefs.getCharPref('b2g.reload_key')); + return (evt.keyCode == key.key && + evt.ctrlKey == key.ctrl && + evt.altKey == key.alt && + evt.shiftKey == key.shift && + evt.metaKey == key.meta); + } catch(e) { + debug('Failed to get key: ' + e); + } + + return false; + } + + let content = this.contentBrowser.contentWindow; + switch (evt.type) { + case 'keydown': + case 'keyup': + if (checkReloadKey()) { + clearCacheAndReload(); + } else { + this.broadcastHardwareKeys(evt); + } + break; + case 'sizemodechange': + if (window.windowState == window.STATE_MINIMIZED && !this.visibleNormalAudioActive) { + this.contentBrowser.setVisible(false); + } else { + this.contentBrowser.setVisible(true); + } + break; + case 'load': + if (content.document.location == 'about:blank') { + return; + } + content.removeEventListener('load', this, true); + this.notifyContentWindowLoaded(); + break; + case 'mozbrowserloadstart': + if (content.document.location == 'about:blank') { + this.contentBrowser.addEventListener('mozbrowserlocationchange', this, true); + return; + } + + this.notifyContentStart(); + break; + case 'mozbrowserlocationchange': + if (content.document.location == 'about:blank') { + return; + } + + this.notifyContentStart(); + break; + case 'mozbrowserscrollviewchange': + this.sendChromeEvent({ + type: 'scrollviewchange', + detail: evt.detail, + }); + break; + case 'mozbrowsercaretstatechanged': + { + let elt = evt.target; + let win = elt.ownerDocument.defaultView; + let offsetX = win.mozInnerScreenX - window.mozInnerScreenX; + let offsetY = win.mozInnerScreenY - window.mozInnerScreenY; + + let rect = elt.getBoundingClientRect(); + offsetX += rect.left; + offsetY += rect.top; + + let data = evt.detail; + data.offsetX = offsetX; + data.offsetY = offsetY; + data.sendDoCommandMsg = null; + + shell.sendChromeEvent({ + type: 'caretstatechanged', + detail: data, + }); + } + break; + + case 'MozApplicationManifest': + try { + if (!Services.prefs.getBoolPref('browser.cache.offline.enable')) + return; + + let contentWindow = evt.originalTarget.defaultView; + let documentElement = contentWindow.document.documentElement; + if (!documentElement) + return; + + let manifest = documentElement.getAttribute('manifest'); + if (!manifest) + return; + + let principal = contentWindow.document.nodePrincipal; + if (Services.perms.testPermissionFromPrincipal(principal, 'offline-app') == Ci.nsIPermissionManager.UNKNOWN_ACTION) { + if (Services.prefs.getBoolPref('browser.offline-apps.notify')) { + // FIXME Bug 710729 - Add a UI for offline cache notifications + return; + } + return; + } + + Services.perms.addFromPrincipal(principal, 'offline-app', + Ci.nsIPermissionManager.ALLOW_ACTION); + + let documentURI = Services.io.newURI(contentWindow.document.documentURI, + null, + null); + let manifestURI = Services.io.newURI(manifest, null, documentURI); + let updateService = Cc['@mozilla.org/offlinecacheupdate-service;1'] + .getService(Ci.nsIOfflineCacheUpdateService); + updateService.scheduleUpdate(manifestURI, documentURI, principal, window); + } catch (e) { + dump('Error while creating offline cache: ' + e + '\n'); + } + break; + case 'MozAfterPaint': + window.removeEventListener('MozAfterPaint', this); + // This event should be sent before System app returns with + // system-message-listener-ready mozContentEvent, because it's on + // the critical launch path of the app. + SystemAppProxy._sendCustomEvent('mozChromeEvent', { + type: 'system-first-paint' + }, /* noPending */ true); + break; + case 'unload': + this.stop(); + break; + } + }, + + // Send an event to a specific window, document or element. + sendEvent: function shell_sendEvent(target, type, details) { + if (target === this.contentBrowser) { + // We must ask SystemAppProxy to send the event in this case so + // that event would be dispatched from frame.contentWindow instead of + // on the System app frame. + SystemAppProxy._sendCustomEvent(type, details); + return; + } + + let doc = target.document || target.ownerDocument || target; + let event = doc.createEvent('CustomEvent'); + event.initCustomEvent(type, true, true, details ? details : {}); + target.dispatchEvent(event); + }, + + sendCustomEvent: function shell_sendCustomEvent(type, details) { + SystemAppProxy._sendCustomEvent(type, details); + }, + + sendChromeEvent: function shell_sendChromeEvent(details) { + this.sendCustomEvent("mozChromeEvent", details); + }, + + receiveMessage: function shell_receiveMessage(message) { + var activities = { 'content-handler': { name: 'view', response: null }, + 'dial-handler': { name: 'dial', response: null }, + 'mail-handler': { name: 'new', response: null }, + 'sms-handler': { name: 'new', response: null }, + 'file-picker': { name: 'pick', response: 'file-picked' } }; + + if (!(message.name in activities)) + return; + + let data = message.data; + let activity = activities[message.name]; + + let a = new MozActivity({ + name: activity.name, + data: data + }); + + if (activity.response) { + a.onsuccess = function() { + let sender = message.target.QueryInterface(Ci.nsIMessageSender); + sender.sendAsyncMessage(activity.response, { success: true, + result: a.result }); + } + a.onerror = function() { + let sender = message.target.QueryInterface(Ci.nsIMessageSender); + sender.sendAsyncMessage(activity.response, { success: false }); + } + } + }, + + notifyContentStart: function shell_notifyContentStart() { + window.performance.mark('gecko-shell-notify-content-start'); + this.contentBrowser.removeEventListener('mozbrowserloadstart', this, true); + this.contentBrowser.removeEventListener('mozbrowserlocationchange', this, true); + + let content = this.contentBrowser.contentWindow; + content.addEventListener('load', this, true); + + this.reportCrash(true); + + SystemAppProxy.registerFrame(shell.contentBrowser); + + this.sendEvent(window, 'ContentStart'); + + Services.obs.notifyObservers(null, 'content-start', null); + + if (AppConstants.MOZ_GRAPHENE && + Services.prefs.getBoolPref("b2g.nativeWindowGeometry.fullscreen")) { + window.fullScreen = true; + } + + shell.handleCmdLine(); + }, + + handleCmdLine: function() { + // This isn't supported on devices. + if (!isGonk) { + let b2gcmds = Cc["@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds"] + .getService(Ci.nsISupports); + let args = b2gcmds.wrappedJSObject.cmdLine; + try { + // Returns null if -url is not present. + let url = args.handleFlagWithParam("url", false); + if (url) { + this.sendChromeEvent({type: "mozbrowseropenwindow", url}); + args.preventDefault = true; + } + } catch(e) { + // Throws if -url is present with no params. + } + } + }, + + // This gets called when window.onload fires on the System app content window, + // which means things in <html> are parsed and statically referenced <script>s + // and <script defer>s are loaded and run. + notifyContentWindowLoaded: function shell_notifyContentWindowLoaded() { + isGonk && libcutils.property_set('sys.boot_completed', '1'); + + // This will cause Gonk Widget to remove boot animation from the screen + // and reveals the page. + Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); + + SystemAppProxy.setIsLoaded(); + }, + + // This gets called when the content sends us system-message-listener-ready + // mozContentEvent, OR when an observer message tell us we should consider + // the content as ready. + notifyEventListenerReady: function shell_notifyEventListenerReady() { + if (this._isEventListenerReady) { + Cu.reportError('shell.js: SystemApp has already been declared as being ready.'); + return; + } + this._isEventListenerReady = true; + + if (Services.prefs.getBoolPref('b2g.orientation.animate')) { + Cu.import('resource://gre/modules/OrientationChangeHandler.jsm'); + } + + SystemAppProxy.setIsReady(); + } +}; + +Services.obs.addObserver(function onFullscreenOriginChange(subject, topic, data) { + shell.sendChromeEvent({ type: "fullscreenoriginchange", + fullscreenorigin: data }); +}, "fullscreen-origin-change", false); + +Services.obs.addObserver(function onBluetoothVolumeChange(subject, topic, data) { + shell.sendChromeEvent({ + type: "bluetooth-volumeset", + value: data + }); +}, 'bluetooth-volume-change', false); + +Services.obs.addObserver(function(subject, topic, data) { + shell.sendCustomEvent('mozmemorypressure'); +}, 'memory-pressure', false); + +Services.obs.addObserver(function(subject, topic, data) { + shell.notifyEventListenerReady(); +}, 'system-message-listener-ready', false); + +var permissionMap = new Map([ + ['unknown', Services.perms.UNKNOWN_ACTION], + ['allow', Services.perms.ALLOW_ACTION], + ['deny', Services.perms.DENY_ACTION], + ['prompt', Services.perms.PROMPT_ACTION], +]); +var permissionMapRev = new Map(Array.from(permissionMap.entries()).reverse()); + +var CustomEventManager = { + init: function custevt_init() { + window.addEventListener("ContentStart", (function(evt) { + let content = shell.contentBrowser.contentWindow; + content.addEventListener("mozContentEvent", this, false, true); + }).bind(this), false); + }, + + handleEvent: function custevt_handleEvent(evt) { + let detail = evt.detail; + dump('XXX FIXME : Got a mozContentEvent: ' + detail.type + "\n"); + + switch(detail.type) { + case 'system-message-listener-ready': + Services.obs.notifyObservers(null, 'system-message-listener-ready', null); + break; + case 'captive-portal-login-cancel': + CaptivePortalLoginHelper.handleEvent(detail); + break; + case 'inputmethod-update-layouts': + case 'inputregistry-add': + case 'inputregistry-remove': + KeyboardHelper.handleEvent(detail); + break; + case 'copypaste-do-command': + Services.obs.notifyObservers({ wrappedJSObject: shell.contentBrowser }, + 'ask-children-to-execute-copypaste-command', detail.cmd); + break; + case 'add-permission': + Services.perms.add(Services.io.newURI(detail.uri, null, null), + detail.permissionType, permissionMap.get(detail.permission)); + break; + case 'remove-permission': + Services.perms.remove(Services.io.newURI(detail.uri, null, null), + detail.permissionType); + break; + case 'test-permission': + let result = Services.perms.testExactPermission( + Services.io.newURI(detail.uri, null, null), detail.permissionType); + // Not equal check here because we want to prevent default only if it's not set + if (result !== permissionMapRev.get(detail.permission)) { + evt.preventDefault(); + } + break; + case 'shutdown-application': + let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'] + .getService(Ci.nsIAppStartup); + appStartup.quit(appStartup.eAttemptQuit); + break; + case 'toggle-fullscreen-native-window': + window.fullScreen = !window.fullScreen; + Services.prefs.setBoolPref("b2g.nativeWindowGeometry.fullscreen", + window.fullScreen); + break; + case 'minimize-native-window': + window.minimize(); + break; + case 'clear-cache-and-reload': + clearCacheAndReload(); + break; + case 'clear-cache-and-restart': + clearCache(); + restart(); + break; + case 'restart': + restart(); + break; + } + } +} + +var KeyboardHelper = { + handleEvent: function keyboard_handleEvent(detail) { + switch (detail.type) { + case 'inputmethod-update-layouts': + Keyboard.setLayouts(detail.layouts); + + break; + case 'inputregistry-add': + case 'inputregistry-remove': + Keyboard.inputRegistryGlue.returnMessage(detail); + + break; + } + } +}; + +// This is the backend for Gaia's screenshot feature. Gaia requests a +// screenshot by sending a mozContentEvent with detail.type set to +// 'take-screenshot'. Then we take a screenshot and send a +// mozChromeEvent with detail.type set to 'take-screenshot-success' +// and detail.file set to the an image/png blob +window.addEventListener('ContentStart', function ss_onContentStart() { + let content = shell.contentBrowser.contentWindow; + content.addEventListener('mozContentEvent', function ss_onMozContentEvent(e) { + if (e.detail.type !== 'take-screenshot') + return; + + try { + shell.sendChromeEvent({ + type: 'take-screenshot-success', + file: Screenshot.get() + }); + } catch (e) { + dump('exception while creating screenshot: ' + e + '\n'); + shell.sendChromeEvent({ + type: 'take-screenshot-error', + error: String(e) + }); + } + }); +}); + +(function contentCrashTracker() { + Services.obs.addObserver(function(aSubject, aTopic, aData) { + let props = aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (props.hasKey("abnormal") && props.hasKey("dumpID")) { + shell.reportCrash(false, props.getProperty("dumpID")); + } + }, + "ipc:content-shutdown", false); +})(); + +var CaptivePortalLoginHelper = { + init: function init() { + Services.obs.addObserver(this, 'captive-portal-login', false); + Services.obs.addObserver(this, 'captive-portal-login-abort', false); + Services.obs.addObserver(this, 'captive-portal-login-success', false); + }, + handleEvent: function handleEvent(detail) { + Services.captivePortalDetector.cancelLogin(detail.id); + }, + observe: function observe(subject, topic, data) { + shell.sendChromeEvent(JSON.parse(data)); + } +} + +// Listen for crashes submitted through the crash reporter UI. +window.addEventListener('ContentStart', function cr_onContentStart() { + let content = shell.contentBrowser.contentWindow; + content.addEventListener("mozContentEvent", function cr_onMozContentEvent(e) { + if (e.detail.type == "submit-crash" && e.detail.crashID) { + debugCrashReport("submitting crash at user request ", e.detail.crashID); + shell.submitCrash(e.detail.crashID); + } else if (e.detail.type == "delete-crash" && e.detail.crashID) { + debugCrashReport("deleting crash at user request ", e.detail.crashID); + shell.deleteCrash(e.detail.crashID); + } + }); +}); + +window.addEventListener('ContentStart', function update_onContentStart() { + if (!AppConstants.MOZ_UPDATER) { + return; + } + + let promptCc = Cc["@mozilla.org/updates/update-prompt;1"]; + if (!promptCc) { + return; + } + + let updatePrompt = promptCc.createInstance(Ci.nsIUpdatePrompt); + if (!updatePrompt) { + return; + } + + updatePrompt.wrappedJSObject.handleContentStart(shell); +}); +/* The "GPSChipOn" is to indicate that GPS engine is turned ON by the modem. + During this GPS engine is turned ON by the modem, we make the location tracking icon visible to user. + Once GPS engine is turned OFF, the location icon will disappear. + If GPS engine is not turned ON by the modem or GPS location service is triggered, + we let GPS service take over the control of showing the location tracking icon. + The regular sequence of the geolocation-device-events is: starting-> GPSStarting-> shutdown-> GPSShutdown +*/ + + +(function geolocationStatusTracker() { + let gGeolocationActive = false; + let GPSChipOn = false; + + Services.obs.addObserver(function(aSubject, aTopic, aData) { + let oldState = gGeolocationActive; + let promptWarning = false; + switch (aData) { + case "GPSStarting": + if (!gGeolocationActive) { + gGeolocationActive = true; + GPSChipOn = true; + promptWarning = true; + } + break; + case "GPSShutdown": + if (GPSChipOn) { + gGeolocationActive = false; + GPSChipOn = false; + } + break; + case "starting": + gGeolocationActive = true; + GPSChipOn = false; + break; + case "shutdown": + gGeolocationActive = false; + break; + } + + if (gGeolocationActive != oldState) { + shell.sendChromeEvent({ + type: 'geolocation-status', + active: gGeolocationActive, + prompt: promptWarning + }); + } +}, "geolocation-device-events", false); +})(); + +(function headphonesStatusTracker() { + Services.obs.addObserver(function(aSubject, aTopic, aData) { + shell.sendChromeEvent({ + type: 'headphones-status-changed', + state: aData + }); +}, "headphones-status-changed", false); +})(); + +(function audioChannelChangedTracker() { + Services.obs.addObserver(function(aSubject, aTopic, aData) { + shell.sendChromeEvent({ + type: 'audio-channel-changed', + channel: aData + }); +}, "audio-channel-changed", false); +})(); + +(function defaultVolumeChannelChangedTracker() { + Services.obs.addObserver(function(aSubject, aTopic, aData) { + shell.sendChromeEvent({ + type: 'default-volume-channel-changed', + channel: aData + }); +}, "default-volume-channel-changed", false); +})(); + +(function visibleAudioChannelChangedTracker() { + Services.obs.addObserver(function(aSubject, aTopic, aData) { + shell.sendChromeEvent({ + type: 'visible-audio-channel-changed', + channel: aData + }); + shell.visibleNormalAudioActive = (aData == 'normal'); +}, "visible-audio-channel-changed", false); +})(); + +(function recordingStatusTracker() { + // Recording status is tracked per process with following data structure: + // {<processId>: {<requestURL>: {isApp: <isApp>, + // count: <N>, + // audioCount: <N>, + // videoCount: <N>}} + let gRecordingActiveProcesses = {}; + + let recordingHandler = function(aSubject, aTopic, aData) { + let props = aSubject.QueryInterface(Ci.nsIPropertyBag2); + let processId = (props.hasKey('childID')) ? props.get('childID') + : 'main'; + if (processId && !gRecordingActiveProcesses.hasOwnProperty(processId)) { + gRecordingActiveProcesses[processId] = {}; + } + + let commandHandler = function (requestURL, command) { + let currentProcess = gRecordingActiveProcesses[processId]; + let currentActive = currentProcess[requestURL]; + let wasActive = (currentActive['count'] > 0); + let wasAudioActive = (currentActive['audioCount'] > 0); + let wasVideoActive = (currentActive['videoCount'] > 0); + + switch (command.type) { + case 'starting': + currentActive['count']++; + currentActive['audioCount'] += (command.isAudio) ? 1 : 0; + currentActive['videoCount'] += (command.isVideo) ? 1 : 0; + break; + case 'shutdown': + currentActive['count']--; + currentActive['audioCount'] -= (command.isAudio) ? 1 : 0; + currentActive['videoCount'] -= (command.isVideo) ? 1 : 0; + break; + case 'content-shutdown': + currentActive['count'] = 0; + currentActive['audioCount'] = 0; + currentActive['videoCount'] = 0; + break; + } + + if (currentActive['count'] > 0) { + currentProcess[requestURL] = currentActive; + } else { + delete currentProcess[requestURL]; + } + + // We need to track changes if any active state is changed. + let isActive = (currentActive['count'] > 0); + let isAudioActive = (currentActive['audioCount'] > 0); + let isVideoActive = (currentActive['videoCount'] > 0); + if ((isActive != wasActive) || + (isAudioActive != wasAudioActive) || + (isVideoActive != wasVideoActive)) { + shell.sendChromeEvent({ + type: 'recording-status', + active: isActive, + requestURL: requestURL, + isApp: currentActive['isApp'], + isAudio: isAudioActive, + isVideo: isVideoActive + }); + } + }; + + switch (aData) { + case 'starting': + case 'shutdown': + // create page record if it is not existed yet. + let requestURL = props.get('requestURL'); + if (requestURL && + !gRecordingActiveProcesses[processId].hasOwnProperty(requestURL)) { + gRecordingActiveProcesses[processId][requestURL] = {isApp: props.get('isApp'), + count: 0, + audioCount: 0, + videoCount: 0}; + } + commandHandler(requestURL, { type: aData, + isAudio: props.get('isAudio'), + isVideo: props.get('isVideo')}); + break; + case 'content-shutdown': + // iterate through all the existing active processes + Object.keys(gRecordingActiveProcesses[processId]).forEach(function(requestURL) { + commandHandler(requestURL, { type: aData, + isAudio: true, + isVideo: true}); + }); + break; + } + + // clean up process record if no page record in it. + if (Object.keys(gRecordingActiveProcesses[processId]).length == 0) { + delete gRecordingActiveProcesses[processId]; + } + }; + Services.obs.addObserver(recordingHandler, 'recording-device-events', false); + Services.obs.addObserver(recordingHandler, 'recording-device-ipc-events', false); + + Services.obs.addObserver(function(aSubject, aTopic, aData) { + // send additional recording events if content process is being killed + let processId = aSubject.QueryInterface(Ci.nsIPropertyBag2).get('childID'); + if (gRecordingActiveProcesses.hasOwnProperty(processId)) { + Services.obs.notifyObservers(aSubject, 'recording-device-ipc-events', 'content-shutdown'); + } + }, 'ipc:content-shutdown', false); +})(); + +(function volumeStateTracker() { + Services.obs.addObserver(function(aSubject, aTopic, aData) { + shell.sendChromeEvent({ + type: 'volume-state-changed', + active: (aData == 'Shared') + }); +}, 'volume-state-changed', false); +})(); + +if (isGonk) { + // Devices don't have all the same partition size for /cache where we + // store the http cache. + (function setHTTPCacheSize() { + let path = Services.prefs.getCharPref("browser.cache.disk.parent_directory"); + let volumeService = Cc["@mozilla.org/telephony/volume-service;1"] + .getService(Ci.nsIVolumeService); + + let stats = volumeService.createOrGetVolumeByPath(path).getStats(); + + // We must set the size in KB, and keep a bit of free space. + let size = Math.floor(stats.totalBytes / 1024) - 1024; + + // keep the default value if it is smaller than the physical partition size. + let oldSize = Services.prefs.getIntPref("browser.cache.disk.capacity"); + if (size < oldSize) { + Services.prefs.setIntPref("browser.cache.disk.capacity", size); + } + })(); + + try { + let gmpService = Cc["@mozilla.org/gecko-media-plugin-service;1"] + .getService(Ci.mozIGeckoMediaPluginChromeService); + gmpService.addPluginDirectory("/system/b2g/gmp-clearkey/0.1"); + } catch(e) { + dump("Failed to add clearkey path! " + e + "\n"); + } +} + +// Calling this observer will cause a shutdown an a profile reset. +// Use eg. : Services.obs.notifyObservers(null, 'b2g-reset-profile', null); +Services.obs.addObserver(function resetProfile(subject, topic, data) { + Services.obs.removeObserver(resetProfile, topic); + + // Listening for 'profile-before-change-telemetry' which is late in the + // shutdown sequence, but still has xpcom access. + Services.obs.addObserver(function clearProfile(subject, topic, data) { + Services.obs.removeObserver(clearProfile, topic); + if (isGonk) { + let json = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile); + json.initWithPath('/system/b2g/webapps/webapps.json'); + let toRemove = json.exists() + // This is a user build, just rm -r /data/local /data/b2g/mozilla + ? ['/data/local', '/data/b2g/mozilla'] + // This is an eng build. We clear the profile and a set of files + // under /data/local. + : ['/data/b2g/mozilla', + '/data/local/permissions.sqlite', + '/data/local/storage', + '/data/local/OfflineCache']; + + toRemove.forEach(function(dir) { + try { + let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile); + file.initWithPath(dir); + file.remove(true); + } catch(e) { dump(e); } + }); + } else { + // Desktop builds. + let profile = Services.dirsvc.get('ProfD', Ci.nsIFile); + + // We don't want to remove everything from the profile, since this + // would prevent us from starting up. + let whitelist = ['defaults', 'extensions', 'settings.json', + 'user.js', 'webapps']; + let enumerator = profile.directoryEntries; + while (enumerator.hasMoreElements()) { + let file = enumerator.getNext().QueryInterface(Ci.nsIFile); + if (whitelist.indexOf(file.leafName) == -1) { + file.remove(true); + } + } + } + }, + 'profile-before-change-telemetry', false); + + let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); +}, 'b2g-reset-profile', false); + +var showInstallScreen; + +if (AppConstants.MOZ_GRAPHENE) { + const restoreWindowGeometry = () => { + let screenX = Services.prefs.getIntPref("b2g.nativeWindowGeometry.screenX"); + let screenY = Services.prefs.getIntPref("b2g.nativeWindowGeometry.screenY"); + let width = Services.prefs.getIntPref("b2g.nativeWindowGeometry.width"); + let height = Services.prefs.getIntPref("b2g.nativeWindowGeometry.height"); + + if (screenX == -1) { + // Center + screenX = (screen.width - width) / 2; + screenY = (screen.height - height) / 2; + } + + moveTo(screenX, screenY); + resizeTo(width, height); + } + restoreWindowGeometry(); + + const saveWindowGeometry = () => { + window.removeEventListener("unload", saveWindowGeometry); + Services.prefs.setIntPref("b2g.nativeWindowGeometry.screenX", screenX); + Services.prefs.setIntPref("b2g.nativeWindowGeometry.screenY", screenY); + Services.prefs.setIntPref("b2g.nativeWindowGeometry.width", outerWidth); + Services.prefs.setIntPref("b2g.nativeWindowGeometry.height", outerHeight); + } + window.addEventListener("unload", saveWindowGeometry); + + var baseWindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIBaseWindow); + + const showNativeWindow = () => baseWindow.visibility = true; + const hideNativeWindow = () => baseWindow.visibility = false; + + showInstallScreen = () => { + const grapheneStrings = + Services.strings.createBundle('chrome://b2g-l10n/locale/graphene.properties'); + document.querySelector('#installing > .message').textContent = + grapheneStrings.GetStringFromName('installing'); + showNativeWindow(); + } + + const hideInstallScreen = () => { + document.body.classList.add('content-loaded'); + } + + window.addEventListener('ContentStart', () => { + shell.contentBrowser.contentWindow.addEventListener('load', () => { + hideInstallScreen(); + showNativeWindow(); + }); + }); + + hideNativeWindow(); +} diff --git a/b2g/chrome/content/shell_remote.html b/b2g/chrome/content/shell_remote.html new file mode 100644 index 000000000..4f3f6efc8 --- /dev/null +++ b/b2g/chrome/content/shell_remote.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml" + id="shellRemote" + windowtype="navigator:remote-browser" + sizemode="fullscreen" + > + +<head> + <link rel="stylesheet" href="shell.css" type="text/css"> + <script type="application/javascript;version=1.8" + src="chrome://b2g/content/shell_remote.js"> </script> +</head> + <body id="container"> + </body> +</html> diff --git a/b2g/chrome/content/shell_remote.js b/b2g/chrome/content/shell_remote.js new file mode 100644 index 000000000..1f1115ef0 --- /dev/null +++ b/b2g/chrome/content/shell_remote.js @@ -0,0 +1,139 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SystemAppProxy.jsm"); + +function debug(aStr) { + // dump(" -*- ShellRemote.js: " + aStr + "\n"); +} + +var remoteShell = { + + _started: false, + + get homeURL() { + let systemAppManifestURL = Services.io.newURI(this.systemAppManifestURL, null, null); + let shellRemoteURL = Services.prefs.getCharPref("b2g.multiscreen.system_remote_url"); + shellRemoteURL = Services.io.newURI(shellRemoteURL, null, systemAppManifestURL); + return shellRemoteURL.spec; + }, + + get systemAppManifestURL() { + return Services.prefs.getCharPref("b2g.system_manifest_url"); + }, + + hasStarted: function () { + return this._started; + }, + + start: function () { + this._started = true; + this._isEventListenerReady = false; + this.id = window.location.hash.substring(1); + + let homeURL = this.homeURL; + if (!homeURL) { + debug("ERROR! Remote home URL undefined."); + return; + } + let manifestURL = this.systemAppManifestURL; + // <html:iframe id="this.id" + // mozbrowser="true" + // allowfullscreen="true" + // src="blank.html"/> + let systemAppFrame = + document.createElementNS("http://www.w3.org/1999/xhtml", "html:iframe"); + systemAppFrame.setAttribute("id", this.id); + systemAppFrame.setAttribute("mozbrowser", "true"); + systemAppFrame.setAttribute("mozapp", manifestURL); + systemAppFrame.setAttribute("allowfullscreen", "true"); + systemAppFrame.setAttribute("src", "blank.html"); + + let container = document.getElementById("container"); + this.contentBrowser = container.appendChild(systemAppFrame); + this.contentBrowser.src = homeURL + window.location.hash; + + window.addEventListener("unload", this); + this.contentBrowser.addEventListener("mozbrowserloadstart", this); + }, + + stop: function () { + window.removeEventListener("unload", this); + this.contentBrowser.removeEventListener("mozbrowserloadstart", this); + this.contentBrowser.removeEventListener("mozbrowserlocationchange", this, true); + SystemAppProxy.unregisterFrameWithId(this.id); + }, + + notifyContentStart: function(evt) { + this.contentBrowser.removeEventListener("mozbrowserloadstart", this); + this.contentBrowser.removeEventListener("mozbrowserlocationchange", this, true); + + SystemAppProxy.registerFrameWithId(remoteShell.id, remoteShell.contentBrowser); + SystemAppProxy.addEventListenerWithId(this.id, "mozContentEvent", this); + + let content = this.contentBrowser.contentWindow; + content.addEventListener("load", this, true); + }, + + notifyContentWindowLoaded: function () { + SystemAppProxy.setIsLoadedWithId(this.id); + }, + + notifyEventListenerReady: function () { + if (this._isEventListenerReady) { + Cu.reportError("shell_remote.js: SystemApp has already been declared as being ready."); + return; + } + this._isEventListenerReady = true; + SystemAppProxy.setIsReadyWithId(this.id); + }, + + handleEvent: function(evt) { + debug("Got an event: " + evt.type); + let content = this.contentBrowser.contentWindow; + + switch(evt.type) { + case "mozContentEvent": + if (evt.detail.type === "system-message-listener-ready") { + this.notifyEventListenerReady(); + } + break; + case "load": + if (content.document.location == "about:blank") { + return; + } + content.removeEventListener("load", this, true); + this.notifyContentWindowLoaded(); + break; + case "mozbrowserloadstart": + if (content.document.location == "about:blank") { + this.contentBrowser.addEventListener("mozbrowserlocationchange", this, true); + return; + } + case "mozbrowserlocationchange": + if (content.document.location == "about:blank") { + return; + } + this.notifyContentStart(); + break; + case "unload": + this.stop(); + break; + } + } +}; + +window.onload = function() { + if (remoteShell.hasStarted() == false) { + remoteShell.start(); + } +}; + diff --git a/b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js b/b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js new file mode 100644 index 000000000..1a5ed8274 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; +const { Services } = Cu.import('resource://gre/modules/Services.jsm'); +const { SystemAppProxy } = Cu.import('resource://gre/modules/SystemAppProxy.jsm'); + +var processId; + +function peekChildId(aSubject, aTopic, aData) { + Services.obs.removeObserver(peekChildId, 'recording-device-events'); + Services.obs.removeObserver(peekChildId, 'recording-device-ipc-events'); + let props = aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (props.hasKey('childID')) { + processId = props.get('childID'); + } +} + +addMessageListener('init-chrome-event', function(message) { + // listen mozChromeEvent and forward to content process. + let type = message.type; + SystemAppProxy.addEventListener('mozChromeEvent', function(event) { + let details = event.detail; + if (details.type === type) { + sendAsyncMessage('chrome-event', details); + } + }, true); + + Services.obs.addObserver(peekChildId, 'recording-device-events', false); + Services.obs.addObserver(peekChildId, 'recording-device-ipc-events', false); +}); + +addMessageListener('fake-content-shutdown', function(message) { + let props = Cc["@mozilla.org/hash-property-bag;1"] + .createInstance(Ci.nsIWritablePropertyBag2); + if (processId) { + props.setPropertyAsUint64('childID', processId); + } + Services.obs.notifyObservers(props, 'recording-device-ipc-events', 'content-shutdown'); +}); diff --git a/b2g/chrome/content/test/mochitest/RecordingStatusHelper.js b/b2g/chrome/content/test/mochitest/RecordingStatusHelper.js new file mode 100644 index 000000000..5e3e6814e --- /dev/null +++ b/b2g/chrome/content/test/mochitest/RecordingStatusHelper.js @@ -0,0 +1,82 @@ +'use strict'; + +// resolve multiple promise in parallel +function expectAll(aValue) { + let deferred = new Promise(function(resolve, reject) { + let countdown = aValue.length; + let resolutionValues = new Array(countdown); + + for (let i = 0; i < aValue.length; i++) { + let index = i; + aValue[i].then(function(val) { + resolutionValues[index] = val; + if (--countdown === 0) { + resolve(resolutionValues); + } + }, reject); + } + }); + + return deferred; +} + +function TestInit() { + let url = SimpleTest.getTestFileURL("RecordingStatusChromeScript.js") + let script = SpecialPowers.loadChromeScript(url); + + let helper = { + finish: function () { + script.destroy(); + }, + fakeShutdown: function () { + script.sendAsyncMessage('fake-content-shutdown', {}); + } + }; + + script.addMessageListener('chrome-event', function (message) { + if (helper.hasOwnProperty('onEvent')) { + helper.onEvent(message); + } else { + ok(false, 'unexpected message: ' + JSON.stringify(message)); + } + }); + + script.sendAsyncMessage("init-chrome-event", { + type: 'recording-status' + }); + + return Promise.resolve(helper); +} + +function expectEvent(expected, eventHelper) { + return new Promise(function(resolve, reject) { + eventHelper.onEvent = function(message) { + delete eventHelper.onEvent; + ok(message, JSON.stringify(message)); + is(message.type, 'recording-status', 'event type: ' + message.type); + is(message.active, expected.active, 'recording active: ' + message.active); + is(message.isAudio, expected.isAudio, 'audio recording active: ' + message.isAudio); + is(message.isVideo, expected.isVideo, 'video recording active: ' + message.isVideo); + resolve(eventHelper); + }; + info('waiting for recording-status'); + }); +} + +function expectStream(params, callback) { + return new Promise(function(resolve, reject) { + var req = navigator.mozGetUserMedia( + params, + function(stream) { + ok(true, 'create media stream'); + callback(stream); + resolve(); + }, + function(err) { + ok(false, 'fail to create media stream'); + reject(err); + } + ); + info('waiting for gUM result'); + }); +} diff --git a/b2g/chrome/content/test/mochitest/file_getusermedia_iframe.html b/b2g/chrome/content/test/mochitest/file_getusermedia_iframe.html new file mode 100644 index 000000000..f2b18eab3 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/file_getusermedia_iframe.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Iframe for Recording Status</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.7" src="RecordingStatusHelper.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript;version=1.7"> + +var localStream; + +window.addEventListener('message', function(event) { + switch (event.data) { + case 'start': + let gumDeferred = expectStream({ audio: true, + fake: true + }, function(stream) { + localStream = stream; + event.source.postMessage('start-finished', window.location.origin); + }); + break; + case 'stop': + localStream.stop(); + localStream = null; + break; + } +}, false); + +</script> +</pre> +</body> +</html> diff --git a/b2g/chrome/content/test/mochitest/mochitest.ini b/b2g/chrome/content/test/mochitest/mochitest.ini new file mode 100644 index 000000000..d18a20401 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/mochitest.ini @@ -0,0 +1,11 @@ +[DEFAULT] +skip-if = ((buildapp == 'mulet' || buildapp == 'b2g') && toolkit != 'gonk') #require OOP support for mochitest-b2g-desktop, Bug 957554 +support-files = + RecordingStatusChromeScript.js + RecordingStatusHelper.js + file_getusermedia_iframe.html + +[test_recordingStatus_basic.html] +[test_recordingStatus_multiple_requests.html] +[test_recordingStatus_iframe.html] +[test_recordingStatus_kill_content_process.html] diff --git a/b2g/chrome/content/test/mochitest/moz.build b/b2g/chrome/content/test/mochitest/moz.build new file mode 100644 index 000000000..3b13ba431 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ['mochitest.ini'] diff --git a/b2g/chrome/content/test/mochitest/test_recordingStatus_basic.html b/b2g/chrome/content/test/mochitest/test_recordingStatus_basic.html new file mode 100644 index 000000000..21f746d33 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/test_recordingStatus_basic.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Recording Status</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.7" src="RecordingStatusHelper.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript;version=1.7"> +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +function test() { + let localStreams = []; + TestInit().then(function(eventHelper) { + /* step 1: create one audio stream + * expect: see one mozChromeEvent for audio recording start. + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: false + }, eventHelper); + + let gumDeferred = expectStream({ audio: true, + fake: true + }, function(stream) { + localStreams.push(stream); + }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 2: close the audio stream + * expect: see one mozChromeEvent for recording stop. + */ + let eventDeferred = expectEvent({ active: false, + isAudio: false, + isVideo: false, + }, eventHelper); + + localStreams.shift().stop(); + info('stop audio stream'); + return eventDeferred; + }).then(function(eventHelper) { + /* step 3: create one video stream + * expect: see one mozChromeEvent for video recording start + */ + let eventDeferred = expectEvent({ active: true, + isAudio: false, + isVideo: true + }, eventHelper); + + let gumDeferred = expectStream({ video: true, + fake: true + }, function(stream) { + localStreams.push(stream); + }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 4: close the audio stream + * expect: see one mozChromeEvent for recording stop. + */ + let eventDeferred = expectEvent({ active: false, + isAudio: false, + isVideo: false, + }, eventHelper); + + localStreams.shift().stop(); + info('stop video stream'); + return eventDeferred; + }).then(function(eventHelper) { + /* step 3: create one audio/video stream + * expect: see one mozChromeEvent for audio/video recording start + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: true + }, eventHelper); + + let gumDeferred = expectStream({ audio: true, + video: true, + fake: true + }, function(stream) { + localStreams.push(stream); + }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 4: close the audio stream + * expect: see one mozChromeEvent for recording stop. + */ + let eventDeferred = expectEvent({ active: false, + isAudio: false, + isVideo: false, + }, eventHelper); + + localStreams.shift().stop(); + info('stop audio/video stream'); + return eventDeferred; + }).then(function(eventHelper) { + eventHelper.finish(); + SimpleTest.finish(); + }); +} + +SpecialPowers.pushPrefEnv({ + "set": [ + ['media.navigator.permission.disabled', true] + ] +}, test); + +</script> +</pre> +</body> +</html> diff --git a/b2g/chrome/content/test/mochitest/test_recordingStatus_iframe.html b/b2g/chrome/content/test/mochitest/test_recordingStatus_iframe.html new file mode 100644 index 000000000..88c33c897 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/test_recordingStatus_iframe.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Recording Status in iframe</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.7" src="RecordingStatusHelper.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<iframe id="gum-iframe"></iframe> +<script class="testbody" type="text/javascript;version=1.7"> +SimpleTest.waitForExplicitFinish(); + +function test() { + TestInit().then(function(eventHelper) { + /* step 1: load iframe whilch creates audio stream + * expect: see one mozChromeEvent for audio recording start. + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: false + }, eventHelper); + + let loadDeferred = new Promise(function(resolve, reject) { + let gumIframe = document.getElementById('gum-iframe'); + gumIframe.src = 'file_getusermedia_iframe.html'; + + window.addEventListener('message', function(event) { + if (event.data === 'start-finished') { + resolve(); + } + }, false); + + gumIframe.onload = function() { + info('start audio stream in iframe'); + gumIframe.contentWindow.postMessage('start', window.location.origin); + }; + }); + + return expectAll([eventDeferred, loadDeferred]); + }).then(function([eventHelper]) { + /* step 2: close the audio stream + * expect: see one mozChromeEvent for recording stop. + */ + let eventDeferred = expectEvent({ active: false, + isAudio: false, + isVideo: false + }, eventHelper); + + let win = document.getElementById('gum-iframe').contentWindow; + win.postMessage('stop', window.location.origin); + info('stop audio stream in iframe'); + return eventDeferred; + }).then(function(eventHelper) { + eventHelper.finish(); + SimpleTest.finish(); + }); +} + +SpecialPowers.pushPrefEnv({ + "set": [ + ['media.navigator.permission.disabled', true] + ] +}, test); + +</script> +</pre> +</body> +</html> diff --git a/b2g/chrome/content/test/mochitest/test_recordingStatus_kill_content_process.html b/b2g/chrome/content/test/mochitest/test_recordingStatus_kill_content_process.html new file mode 100644 index 000000000..239c2c2d5 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/test_recordingStatus_kill_content_process.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Recording Status after process shutdown</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.7" src="RecordingStatusHelper.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript;version=1.7"> +SimpleTest.waitForExplicitFinish(); + +function test() { + let localStreams = []; + TestInit().then(function(eventHelper) { + /* step 1: load iframe whilch creates audio stream + * expect: see one mozChromeEvent for audio recording start. + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: false + }, eventHelper); + + let gumDeferred = expectStream({ audio: true, + fake: true + }, function(stream) { localStreams.push(stream); }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 2: create video stream + * expect: see one mozChromeEvent for audio recording start. + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: true + }, eventHelper); + + let gumDeferred = expectStream({ video: true, + fake: true + }, function(stream) { localStreams.push(stream); }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 3: close the audio stream + * expect: see one mozChromeEvent for recording stop. + */ + let eventDeferred = expectEvent({ active: false, + isAudio: false, + isVideo: false + }, eventHelper); + + eventHelper.fakeShutdown(); + info('simulate content process been killed'); + return eventDeferred; + }).then(function(eventHelper) { + eventHelper.finish(); + SimpleTest.finish(); + }); +} + +SpecialPowers.pushPrefEnv({ + "set": [ + ['media.navigator.permission.disabled', true] + ] +}, test); + +</script> +</pre> +</body> +</html> diff --git a/b2g/chrome/content/test/mochitest/test_recordingStatus_multiple_requests.html b/b2g/chrome/content/test/mochitest/test_recordingStatus_multiple_requests.html new file mode 100644 index 000000000..7d31a94f8 --- /dev/null +++ b/b2g/chrome/content/test/mochitest/test_recordingStatus_multiple_requests.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Recording Status with multiple gUM requests</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.7" src="RecordingStatusHelper.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript;version=1.7"> +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +function test() { + let localStreams = []; + TestInit().then(function(eventHelper) { + /* step 1: create one audio stream + * expect: see one mozChromeEvent for recording start. + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: false + }, eventHelper); + + let gumDeferred = expectStream({ audio: true, + fake: true + }, function(stream) { + localStreams.push(stream); + }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 2: create another audio stream + * expect: no mozChromeEvent after audio stream is created + */ + let gumDeferred = expectStream({ audio: true, + fake: true + }, function(stream) { + localStreams.push(stream); + }); + + return expectAll([Promise.resolve(eventHelper), gumDeferred]); + }).then(function([eventHelper]) { + /* step 3: create video stream + * expect: see one mozChromeEvent for recording start + */ + let eventDeferred = expectEvent({ active: true, + isAudio: true, + isVideo: true + }, eventHelper); + + let gumDeferred = expectStream({ video: true, + fake: true + }, function(stream) { + localStreams.push(stream); + }); + + return expectAll([eventDeferred, gumDeferred]); + }).then(function([eventHelper]) { + /* step 4: stop first audio stream + * expect: no mozChromeEvent after first audio stream is stopped + */ + localStreams.shift().stop(); + info('stop the first audio stream'); + return Promise.resolve(eventHelper); + }).then(function(eventHelper) { + /* step 5: stop the second audio stream + * expect: see one mozChromeEvent for audio recording stop. + */ + let eventDeferred = expectEvent({ active: true, + isAudio: false, + isVideo: true + }, eventHelper); + + localStreams.shift().stop(); + info('stop the second audio stream'); + return eventDeferred; + }).then(function(eventHelper) { + /* step 6: stop the video stream + * expect: see one mozChromeEvent for video recording stop. + */ + let eventDeferred = expectEvent({ active: false, + isAudio: false, + isVideo: false + }, eventHelper); + + localStreams.shift().stop(); + info('stop the video stream'); + return eventDeferred; + }).then(function(eventHelper) { + eventHelper.finish(); + SimpleTest.finish(); + }); +} + +SpecialPowers.pushPrefEnv({ + "set": [ + ['media.navigator.permission.disabled', true] + ] +}, test); + +</script> +</pre> +</body> +</html> diff --git a/b2g/chrome/content/touchcontrols.css b/b2g/chrome/content/touchcontrols.css new file mode 100644 index 000000000..7c407c331 --- /dev/null +++ b/b2g/chrome/content/touchcontrols.css @@ -0,0 +1,233 @@ +/* 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/. */ + +@namespace url(http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul); + +/* video controls */ +.controlsOverlay { + -moz-box-pack: center; + -moz-box-align: end; + -moz-box-flex: 1; + -moz-box-orient: horizontal; +} + +.controlsOverlay[scaled] { + /* scaled attribute in videocontrols.css causes conflict + due to different -moz-box-orient values */ + -moz-box-align: end; +} + +.controlBar { + -moz-box-flex: 1; + background-color: rgba(50,50,50,0.8); + width: 100%; +} + +.buttonsBar { + -moz-box-flex: 1; + -moz-box-align: center; +} + +.controlsSpacer { + display: none; + -moz-box-flex: 0; +} + +.fullscreenButton, +.playButton, +.castingButton, +.muteButton { + -moz-appearance: none; + padding: 2px; + border: none !important; + min-height: 24px; + min-width: 24px; +} + +.fullscreenButton { + background: url("chrome://b2g/content/images/fullscreen-hdpi.png") no-repeat center; + background-size: contain; + background-origin: content-box; +} + +.fullscreenButton[fullscreened="true"] { + background: url("chrome://b2g/content/images/exitfullscreen-hdpi.png") no-repeat center; + background-size: contain; + background-origin: content-box; +} + +.controlBar[fullscreen-unavailable] .fullscreenButton { + display: none; +} + +.playButton { + background: url("chrome://b2g/content/images/pause-hdpi.png") no-repeat center; + background-size: contain; + background-origin: content-box; +} + +/* + * Normally the button bar has fullscreen spacer play spacer mute, but if + * this is an audio control rather than a video control, the fullscreen button + * is hidden by videocontrols.xml, and that alters the position of the + * play button. This workaround moves it back to center. + */ +.controlBar[fullscreen-unavailable] .playButton { + transform: translateX(28px); +} + +.playButton[paused="true"] { + background: url("chrome://b2g/content/images/play-hdpi.png") no-repeat center; + background-size: contain; + background-origin: content-box; +} + +.castingButton { + display: none; +} + +.muteButton { + background: url("chrome://b2g/content/images/mute-hdpi.png") no-repeat center; + background-size: contain; + background-origin: content-box; +} + +.muteButton[muted="true"] { + background: url("chrome://b2g/content/images/unmute-hdpi.png") no-repeat center; + background-size: contain; + background-origin: content-box; +} + +/* bars */ +.scrubberStack { + -moz-box-flex: 1; + padding: 0px 18px; +} + +.flexibleBar, +.flexibleBar .progress-bar, +.bufferBar, +.bufferBar .progress-bar, +.progressBar, +.progressBar .progress-bar, +.scrubber, +.scrubber .scale-slider, +.scrubber .scale-thumb { + -moz-appearance: none; + border: none; + padding: 0px; + margin: 0px; + background-color: transparent; +} + +.flexibleBar, +.bufferBar, +.progressBar { + height: 24px; + padding: 11px 0px; +} + +.flexibleBar { + padding: 12px 0px; +} + +.flexibleBar .progress-bar { + border: 1px #777777 solid; + border-radius: 1px; +} + +.bufferBar .progress-bar { + border: 2px #AFB1B3 solid; + border-radius: 2px; +} + +.progressBar .progress-bar { + border: 2px #FF9500 solid; + border-radius: 2px; +} + + +.scrubber { + margin-left: -8px; + margin-right: -8px; +} + +.positionLabel, .durationLabel { + font-family: 'Roboto', Helvetica, Arial, sans-serif; + font-size: 12px; + color: white; +} + +.scrubber .scale-thumb { + display: -moz-box; + margin: 0px !important; + padding: 0px !important; + background: url("chrome://b2g/content/images/scrubber-hdpi.png") no-repeat; + background-size: 12px 12px; + height: 12px; + width: 12px; +} + +.statusOverlay { + -moz-box-align: center; + -moz-box-pack: center; + background-color: rgb(50,50,50); +} + +.statusIcon { + margin-bottom: 28px; + width: 36px; + height: 36px; +} + +.statusIcon[type="throbber"] { + background: url("chrome://b2g/content/images/throbber.png") no-repeat center; +} + +.statusIcon[type="error"] { + background: url("chrome://b2g/content/images/error.png") no-repeat center; +} + +/* CSS Transitions */ +.controlBar:not([immediate]) { + transition-property: opacity; + transition-duration: 200ms; +} + +.controlBar[fadeout] { + opacity: 0; +} + +.statusOverlay:not([immediate]) { + transition-property: opacity; + transition-duration: 300ms; + transition-delay: 750ms; +} + +.statusOverlay[fadeout] { + opacity: 0; +} + +.volumeStack, +.controlBar[firstshow="true"] .fullscreenButton, +.controlBar[firstshow="true"] .muteButton, +.controlBar[firstshow="true"] .scrubberStack, +.controlBar[firstshow="true"] .durationBox, +.timeLabel { + display: none; +} + +/* Error description formatting */ +.errorLabel { + font-family: Helvetica, Arial, sans-serif; + font-size: 11px; + color: #bbb; + text-shadow: + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; + padding: 0 10px; + text-align: center; +} |