tenfourfox/b2g/chrome/content/shell.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

1452 lines
50 KiB
JavaScript

/* -*- 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/ContactService.jsm');
Cu.import('resource://gre/modules/DataStoreChangeNotifier.jsm');
Cu.import('resource://gre/modules/AlarmService.jsm');
Cu.import('resource://gre/modules/ActivitiesService.jsm');
Cu.import('resource://gre/modules/NotificationDB.jsm');
Cu.import('resource://gre/modules/Payment.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/RequestSyncService.jsm');
Cu.import('resource://gre/modules/SystemUpdateService.jsm');
if (isGonk) {
Cu.import('resource://gre/modules/MultiscreenHandler.jsm');
Cu.import('resource://gre/modules/NetworkStatsService.jsm');
Cu.import('resource://gre/modules/ResourceStatsService.jsm');
}
Cu.import('resource://gre/modules/KillSwitchMain.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/MobileIdentityManager.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");
Cu.import('resource://gre/modules/Webapps.jsm');
DOMApplicationRegistry.allAppsLaunchable = true;
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');
if (AppConstants.MOZ_SAFE_BROWSING) {
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() {
if (AppConstants.MOZ_B2GDROID) {
Cc["@mozilla.org/b2g/b2gdroid-setup;1"]
.getService(Ci.nsIObserver).observe(window, "shell-startup", null);
}
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('mozbrowserselectionstatechanged', this, true);
this.contentBrowser.addEventListener('mozbrowserscrollviewchange', this, true);
this.contentBrowser.addEventListener('mozbrowsercaretstatechanged', this);
CustomEventManager.init();
WebappsHelper.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);
if (AppConstants.MOZ_SAFE_BROWSING) {
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('mozbrowserselectionstatechanged', 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 'mozbrowserselectionstatechanged':
// The mozbrowserselectionstatechanged event, may have crossed the chrome-content boundary.
// This event always dispatch to shell.js. But the offset we got from this event is
// based on tab's coordinate. So get the actual offsets between shell and evt.target.
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;
DoCommandHelper.setEvent(evt);
shell.sendChromeEvent({
type: 'selectionstatechanged',
detail: data,
});
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);
isGonk && Cu.import('resource://gre/modules/OperatorApps.jsm');
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 && !AppConstants.MOZ_B2GDROID) {
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);
DOMApplicationRegistry.registryReady.then(function () {
// 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: 'webapps-registry-ready'
}, /* noPending */ true);
});
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 'webapps-install-granted':
case 'webapps-install-denied':
case 'webapps-uninstall-granted':
case 'webapps-uninstall-denied':
WebappsHelper.handleEvent(detail);
break;
case 'select-choicechange':
FormsHelper.handleEvent(detail);
break;
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 'do-command':
DoCommandHelper.handleEvent(detail.cmd);
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 DoCommandHelper = {
_event: null,
setEvent: function docommand_setEvent(evt) {
this._event = evt;
},
handleEvent: function docommand_handleEvent(cmd) {
if (this._event) {
Services.obs.notifyObservers({ wrappedJSObject: this._event.target },
'copypaste-docommand', cmd);
this._event = null;
}
}
}
var WebappsHelper = {
_installers: {},
_count: 0,
init: function webapps_init() {
Services.obs.addObserver(this, "webapps-launch", false);
Services.obs.addObserver(this, "webapps-ask-install", false);
Services.obs.addObserver(this, "webapps-ask-uninstall", false);
Services.obs.addObserver(this, "webapps-close", false);
},
registerInstaller: function webapps_registerInstaller(data) {
let id = "installer" + this._count++;
this._installers[id] = data;
return id;
},
handleEvent: function webapps_handleEvent(detail) {
if (!detail || !detail.id)
return;
let installer = this._installers[detail.id];
delete this._installers[detail.id];
switch (detail.type) {
case "webapps-install-granted":
DOMApplicationRegistry.confirmInstall(installer);
break;
case "webapps-install-denied":
DOMApplicationRegistry.denyInstall(installer);
break;
case "webapps-uninstall-granted":
DOMApplicationRegistry.confirmUninstall(installer);
break;
case "webapps-uninstall-denied":
DOMApplicationRegistry.denyUninstall(installer);
break;
}
},
observe: function webapps_observe(subject, topic, data) {
let json = JSON.parse(data);
json.mm = subject;
let id;
switch(topic) {
case "webapps-launch":
DOMApplicationRegistry.getManifestFor(json.manifestURL).then((aManifest) => {
if (!aManifest)
return;
let manifest = new ManifestHelper(aManifest, json.origin,
json.manifestURL);
let payload = {
timestamp: json.timestamp,
url: manifest.fullLaunchPath(json.startPoint),
manifestURL: json.manifestURL
};
shell.sendCustomEvent("webapps-launch", payload);
});
break;
case "webapps-ask-install":
id = this.registerInstaller(json);
shell.sendChromeEvent({
type: "webapps-ask-install",
id: id,
app: json.app
});
break;
case "webapps-ask-uninstall":
id = this.registerInstaller(json);
shell.sendChromeEvent({
type: "webapps-ask-uninstall",
id: id,
app: json.app
});
break;
case "webapps-close":
shell.sendCustomEvent("webapps-close", {
"manifestURL": json.manifestURL
});
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() {
Cu.import('resource://gre/modules/WebappsUpdater.jsm');
WebappsUpdater.handleContentStart(shell);
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);
});
(function geolocationStatusTracker() {
let gGeolocationActive = false;
Services.obs.addObserver(function(aSubject, aTopic, aData) {
let oldState = gGeolocationActive;
if (aData == "starting") {
gGeolocationActive = true;
} else if (aData == "shutdown") {
gGeolocationActive = false;
}
if (gGeolocationActive != oldState) {
shell.sendChromeEvent({
type: 'geolocation-status',
active: gGeolocationActive
});
}
}, "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-change2' 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-change2', false);
let appStartup = Cc['@mozilla.org/toolkit/app-startup;1']
.getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eForceQuit);
}, 'b2g-reset-profile', false);
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;
const 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();
}