diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /b2g/chrome/content/shell.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'b2g/chrome/content/shell.js')
-rw-r--r-- | b2g/chrome/content/shell.js | 1308 |
1 files changed, 1308 insertions, 0 deletions
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(); +} |