summaryrefslogtreecommitdiffstats
path: root/b2g/chrome
diff options
context:
space:
mode:
Diffstat (limited to 'b2g/chrome')
-rw-r--r--b2g/chrome/content/ErrorPage.js73
-rw-r--r--b2g/chrome/content/aboutCertError.xhtml233
-rw-r--r--b2g/chrome/content/arrow.svg5
-rw-r--r--b2g/chrome/content/blank.css7
-rw-r--r--b2g/chrome/content/blank.html10
-rw-r--r--b2g/chrome/content/content.css321
-rw-r--r--b2g/chrome/content/desktop.css59
-rw-r--r--b2g/chrome/content/desktop.js179
-rw-r--r--b2g/chrome/content/devtools/adb.js233
-rw-r--r--b2g/chrome/content/devtools/debugger.js397
-rw-r--r--b2g/chrome/content/devtools/hud.js1017
-rw-r--r--b2g/chrome/content/identity.js166
-rw-r--r--b2g/chrome/content/images/arrowdown-16.pngbin0 -> 246 bytes
-rw-r--r--b2g/chrome/content/images/arrowright-16.pngbin0 -> 235 bytes
-rw-r--r--b2g/chrome/content/images/desktop/home-black.pngbin0 -> 331 bytes
-rw-r--r--b2g/chrome/content/images/desktop/home-white.pngbin0 -> 276 bytes
-rw-r--r--b2g/chrome/content/images/desktop/rotate.pngbin0 -> 657 bytes
-rw-r--r--b2g/chrome/content/images/error.pngbin0 -> 433 bytes
-rw-r--r--b2g/chrome/content/images/errorpage-larry-black.pngbin0 -> 850 bytes
-rw-r--r--b2g/chrome/content/images/errorpage-larry-white.pngbin0 -> 886 bytes
-rw-r--r--b2g/chrome/content/images/errorpage-warning.pngbin0 -> 631 bytes
-rw-r--r--b2g/chrome/content/images/exitfullscreen-hdpi.pngbin0 -> 3409 bytes
-rw-r--r--b2g/chrome/content/images/fullscreen-hdpi.pngbin0 -> 3382 bytes
-rw-r--r--b2g/chrome/content/images/mute-hdpi.pngbin0 -> 3217 bytes
-rw-r--r--b2g/chrome/content/images/pause-hdpi.pngbin0 -> 3042 bytes
-rw-r--r--b2g/chrome/content/images/play-hdpi.pngbin0 -> 3318 bytes
-rw-r--r--b2g/chrome/content/images/scrubber-hdpi.pngbin0 -> 3967 bytes
-rw-r--r--b2g/chrome/content/images/throbber.pngbin0 -> 11862 bytes
-rw-r--r--b2g/chrome/content/images/unmute-hdpi.pngbin0 -> 3259 bytes
-rw-r--r--b2g/chrome/content/netError.css131
-rw-r--r--b2g/chrome/content/screen.js276
-rw-r--r--b2g/chrome/content/settings.js698
-rw-r--r--b2g/chrome/content/shell.css81
-rw-r--r--b2g/chrome/content/shell.html66
-rw-r--r--b2g/chrome/content/shell.js1308
-rw-r--r--b2g/chrome/content/shell_remote.html19
-rw-r--r--b2g/chrome/content/shell_remote.js139
-rw-r--r--b2g/chrome/content/test/mochitest/RecordingStatusChromeScript.js40
-rw-r--r--b2g/chrome/content/test/mochitest/RecordingStatusHelper.js82
-rw-r--r--b2g/chrome/content/test/mochitest/file_getusermedia_iframe.html36
-rw-r--r--b2g/chrome/content/test/mochitest/mochitest.ini11
-rw-r--r--b2g/chrome/content/test/mochitest/moz.build7
-rw-r--r--b2g/chrome/content/test/mochitest/test_recordingStatus_basic.html119
-rw-r--r--b2g/chrome/content/test/mochitest/test_recordingStatus_iframe.html71
-rw-r--r--b2g/chrome/content/test/mochitest/test_recordingStatus_kill_content_process.html72
-rw-r--r--b2g/chrome/content/test/mochitest/test_recordingStatus_multiple_requests.html108
-rw-r--r--b2g/chrome/content/touchcontrols.css233
-rw-r--r--b2g/chrome/jar.mn60
-rw-r--r--b2g/chrome/moz.build13
49 files changed, 6270 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
new file mode 100644
index 000000000..c982426f2
--- /dev/null
+++ b/b2g/chrome/content/images/arrowdown-16.png
Binary files differ
diff --git a/b2g/chrome/content/images/arrowright-16.png b/b2g/chrome/content/images/arrowright-16.png
new file mode 100644
index 000000000..859e98ba6
--- /dev/null
+++ b/b2g/chrome/content/images/arrowright-16.png
Binary files differ
diff --git a/b2g/chrome/content/images/desktop/home-black.png b/b2g/chrome/content/images/desktop/home-black.png
new file mode 100644
index 000000000..c51187ed4
--- /dev/null
+++ b/b2g/chrome/content/images/desktop/home-black.png
Binary files differ
diff --git a/b2g/chrome/content/images/desktop/home-white.png b/b2g/chrome/content/images/desktop/home-white.png
new file mode 100644
index 000000000..43379d0e9
--- /dev/null
+++ b/b2g/chrome/content/images/desktop/home-white.png
Binary files differ
diff --git a/b2g/chrome/content/images/desktop/rotate.png b/b2g/chrome/content/images/desktop/rotate.png
new file mode 100644
index 000000000..9da1b5674
--- /dev/null
+++ b/b2g/chrome/content/images/desktop/rotate.png
Binary files differ
diff --git a/b2g/chrome/content/images/error.png b/b2g/chrome/content/images/error.png
new file mode 100644
index 000000000..58e37283a
--- /dev/null
+++ b/b2g/chrome/content/images/error.png
Binary files differ
diff --git a/b2g/chrome/content/images/errorpage-larry-black.png b/b2g/chrome/content/images/errorpage-larry-black.png
new file mode 100644
index 000000000..9f2e4a6e7
--- /dev/null
+++ b/b2g/chrome/content/images/errorpage-larry-black.png
Binary files differ
diff --git a/b2g/chrome/content/images/errorpage-larry-white.png b/b2g/chrome/content/images/errorpage-larry-white.png
new file mode 100644
index 000000000..fc153c731
--- /dev/null
+++ b/b2g/chrome/content/images/errorpage-larry-white.png
Binary files differ
diff --git a/b2g/chrome/content/images/errorpage-warning.png b/b2g/chrome/content/images/errorpage-warning.png
new file mode 100644
index 000000000..8bf9d8e7d
--- /dev/null
+++ b/b2g/chrome/content/images/errorpage-warning.png
Binary files differ
diff --git a/b2g/chrome/content/images/exitfullscreen-hdpi.png b/b2g/chrome/content/images/exitfullscreen-hdpi.png
new file mode 100644
index 000000000..826e53408
--- /dev/null
+++ b/b2g/chrome/content/images/exitfullscreen-hdpi.png
Binary files differ
diff --git a/b2g/chrome/content/images/fullscreen-hdpi.png b/b2g/chrome/content/images/fullscreen-hdpi.png
new file mode 100644
index 000000000..980e78731
--- /dev/null
+++ b/b2g/chrome/content/images/fullscreen-hdpi.png
Binary files differ
diff --git a/b2g/chrome/content/images/mute-hdpi.png b/b2g/chrome/content/images/mute-hdpi.png
new file mode 100644
index 000000000..6daf7cf71
--- /dev/null
+++ b/b2g/chrome/content/images/mute-hdpi.png
Binary files differ
diff --git a/b2g/chrome/content/images/pause-hdpi.png b/b2g/chrome/content/images/pause-hdpi.png
new file mode 100644
index 000000000..c7837f822
--- /dev/null
+++ b/b2g/chrome/content/images/pause-hdpi.png
Binary files differ
diff --git a/b2g/chrome/content/images/play-hdpi.png b/b2g/chrome/content/images/play-hdpi.png
new file mode 100644
index 000000000..fd64f9697
--- /dev/null
+++ b/b2g/chrome/content/images/play-hdpi.png
Binary files differ
diff --git a/b2g/chrome/content/images/scrubber-hdpi.png b/b2g/chrome/content/images/scrubber-hdpi.png
new file mode 100644
index 000000000..b965b73d5
--- /dev/null
+++ b/b2g/chrome/content/images/scrubber-hdpi.png
Binary files differ
diff --git a/b2g/chrome/content/images/throbber.png b/b2g/chrome/content/images/throbber.png
new file mode 100644
index 000000000..c601ec80b
--- /dev/null
+++ b/b2g/chrome/content/images/throbber.png
Binary files differ
diff --git a/b2g/chrome/content/images/unmute-hdpi.png b/b2g/chrome/content/images/unmute-hdpi.png
new file mode 100644
index 000000000..5de342bda
--- /dev/null
+++ b/b2g/chrome/content/images/unmute-hdpi.png
Binary files differ
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;
+}
diff --git a/b2g/chrome/jar.mn b/b2g/chrome/jar.mn
new file mode 100644
index 000000000..1fdea9a50
--- /dev/null
+++ b/b2g/chrome/jar.mn
@@ -0,0 +1,60 @@
+#filter substitution
+# 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/.
+
+
+chrome.jar:
+% content branding %content/branding/ contentaccessible=yes
+% content b2g %content/
+
+ content/arrow.svg (content/arrow.svg)
+ content/settings.js (content/settings.js)
+* content/shell.html (content/shell.html)
+ content/shell.js (content/shell.js)
+ content/shell_remote.html (content/shell_remote.html)
+ content/shell_remote.js (content/shell_remote.js)
+* content/shell.css (content/shell.css)
+ content/blank.html (content/blank.html)
+ content/blank.css (content/blank.css)
+#ifdef MOZ_WIDGET_GONK
+ content/devtools/adb.js (content/devtools/adb.js)
+#endif
+ content/devtools/debugger.js (content/devtools/debugger.js)
+ content/devtools/hud.js (content/devtools/hud.js)
+#ifndef MOZ_WIDGET_GONK
+ content/desktop.css (content/desktop.css)
+ content/images/desktop/home-black.png (content/images/desktop/home-black.png)
+ content/images/desktop/home-white.png (content/images/desktop/home-white.png)
+ content/images/desktop/rotate.png (content/images/desktop/rotate.png)
+ content/desktop.js (content/desktop.js)
+ content/screen.js (content/screen.js)
+#endif
+* content/content.css (content/content.css)
+ content/touchcontrols.css (content/touchcontrols.css)
+
+ content/identity.js (content/identity.js)
+
+#ifndef MOZ_GRAPHENE
+% override chrome://global/skin/media/videocontrols.css chrome://b2g/content/touchcontrols.css
+#endif
+% override chrome://global/content/aboutCertError.xhtml chrome://b2g/content/aboutCertError.xhtml
+% override chrome://global/skin/netError.css chrome://b2g/content/netError.css
+
+ content/ErrorPage.js (content/ErrorPage.js)
+ content/aboutCertError.xhtml (content/aboutCertError.xhtml)
+ content/netError.css (content/netError.css)
+ content/images/errorpage-larry-black.png (content/images/errorpage-larry-black.png)
+ content/images/errorpage-larry-white.png (content/images/errorpage-larry-white.png)
+ content/images/errorpage-warning.png (content/images/errorpage-warning.png)
+ content/images/arrowdown-16.png (content/images/arrowdown-16.png)
+ content/images/arrowright-16.png (content/images/arrowright-16.png)
+ content/images/scrubber-hdpi.png (content/images/scrubber-hdpi.png)
+ content/images/unmute-hdpi.png (content/images/unmute-hdpi.png)
+ content/images/pause-hdpi.png (content/images/pause-hdpi.png)
+ content/images/play-hdpi.png (content/images/play-hdpi.png)
+ content/images/mute-hdpi.png (content/images/mute-hdpi.png)
+ content/images/fullscreen-hdpi.png (content/images/fullscreen-hdpi.png)
+ content/images/exitfullscreen-hdpi.png (content/images/exitfullscreen-hdpi.png)
+ content/images/throbber.png (content/images/throbber.png)
+ content/images/error.png (content/images/error.png)
diff --git a/b2g/chrome/moz.build b/b2g/chrome/moz.build
new file mode 100644
index 000000000..99af44a5c
--- /dev/null
+++ b/b2g/chrome/moz.build
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+DEFINES['AB_CD'] = CONFIG['MOZ_UI_LOCALE']
+DEFINES['PACKAGE'] = 'b2g'
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+
+JAR_MANIFESTS += ['jar.mn']
+
+TEST_DIRS += ['content/test/mochitest']