/* -*- 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;
//
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 are parsed and statically referenced