diff -r 000000000000 -r 6474c204b198 dom/browser-element/BrowserElementParent.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dom/browser-element/BrowserElementParent.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,855 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let Cu = Components.utils; +let Ci = Components.interfaces; +let Cc = Components.classes; +let Cr = Components.results; + +/* BrowserElementParent injects script to listen for certain events in the + * child. We then listen to messages from the child script and take + * appropriate action here in the parent. + */ + +this.EXPORTED_SYMBOLS = ["BrowserElementParentBuilder"]; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/BrowserElementPromptService.jsm"); + +XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function () { + Cu.import("resource://gre/modules/Webapps.jsm"); + return DOMApplicationRegistry; +}); + +const TOUCH_EVENTS_ENABLED_PREF = "dom.w3c_touch_events.enabled"; + +function debug(msg) { + //dump("BrowserElementParent.jsm - " + msg + "\n"); +} + +function getIntPref(prefName, def) { + try { + return Services.prefs.getIntPref(prefName); + } + catch(err) { + return def; + } +} + +function exposeAll(obj) { + // Filter for Objects and Arrays. + if (typeof obj !== "object" || !obj) + return; + + // Recursively expose our children. + Object.keys(obj).forEach(function(key) { + exposeAll(obj[key]); + }); + + // If we're not an Array, generate an __exposedProps__ object for ourselves. + if (obj instanceof Array) + return; + var exposed = {}; + Object.keys(obj).forEach(function(key) { + exposed[key] = 'rw'; + }); + obj.__exposedProps__ = exposed; +} + +function defineAndExpose(obj, name, value) { + obj[name] = value; + if (!('__exposedProps__' in obj)) + obj.__exposedProps__ = {}; + obj.__exposedProps__[name] = 'r'; +} + +function visibilityChangeHandler(e) { + // The visibilitychange event's target is the document. + let win = e.target.defaultView; + + if (!win._browserElementParents) { + return; + } + + let beps = Cu.nondeterministicGetWeakMapKeys(win._browserElementParents); + if (beps.length == 0) { + win.removeEventListener('visibilitychange', visibilityChangeHandler); + return; + } + + for (let i = 0; i < beps.length; i++) { + beps[i]._ownerVisibilityChange(); + } +} + +this.BrowserElementParentBuilder = { + create: function create(frameLoader, hasRemoteFrame, isPendingFrame) { + return new BrowserElementParent(frameLoader, hasRemoteFrame); + } +} + + +// The active input method iframe. +let activeInputFrame = null; + +function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) { + debug("Creating new BrowserElementParent object for " + frameLoader); + this._domRequestCounter = 0; + this._pendingDOMRequests = {}; + this._hasRemoteFrame = hasRemoteFrame; + this._nextPaintListeners = []; + + this._frameLoader = frameLoader; + this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement; + let self = this; + if (!this._frameElement) { + debug("No frame element?"); + return; + } + + Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true); + Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true); + + let defineMethod = function(name, fn) { + XPCNativeWrapper.unwrap(self._frameElement)[name] = function() { + if (self._isAlive()) { + return fn.apply(self, arguments); + } + }; + } + + let defineNoReturnMethod = function(name, fn) { + XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() { + if (!self._mm) { + // Remote browser haven't been created, we just queue the API call. + let args = Array.slice(arguments); + args.unshift(self); + self._pendingAPICalls.push(method.bind.apply(fn, args)); + return; + } + if (self._isAlive()) { + fn.apply(self, arguments); + } + }; + }; + + let defineDOMRequestMethod = function(domName, msgName) { + XPCNativeWrapper.unwrap(self._frameElement)[domName] = function() { + if (!self._mm) { + return self._queueDOMRequest; + } + if (self._isAlive()) { + return self._sendDOMRequest(msgName); + } + }; + } + + // Define methods on the frame element. + defineNoReturnMethod('setVisible', this._setVisible); + defineDOMRequestMethod('getVisible', 'get-visible'); + defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent); + + // 0 = disabled, 1 = enabled, 2 - auto detect + if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) { + defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent); + } + defineNoReturnMethod('goBack', this._goBack); + defineNoReturnMethod('goForward', this._goForward); + defineNoReturnMethod('reload', this._reload); + defineNoReturnMethod('stop', this._stop); + defineDOMRequestMethod('purgeHistory', 'purge-history'); + defineMethod('getScreenshot', this._getScreenshot); + defineMethod('addNextPaintListener', this._addNextPaintListener); + defineMethod('removeNextPaintListener', this._removeNextPaintListener); + defineDOMRequestMethod('getCanGoBack', 'get-can-go-back'); + defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward'); + + let principal = this._frameElement.ownerDocument.nodePrincipal; + let perm = Services.perms + .testExactPermissionFromPrincipal(principal, "input-manage"); + if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) { + defineMethod('setInputMethodActive', this._setInputMethodActive); + } + + // Listen to visibilitychange on the iframe's owner window, and forward + // changes down to the child. We want to do this while registering as few + // visibilitychange listeners on _window as possible, because such a listener + // may live longer than this BrowserElementParent object. + // + // To accomplish this, we register just one listener on the window, and have + // it reference a WeakMap whose keys are all the BrowserElementParent objects + // on the window. Then when the listener fires, we iterate over the + // WeakMap's keys (which we can do, because we're chrome) to notify the + // BrowserElementParents. + if (!this._window._browserElementParents) { + this._window._browserElementParents = new WeakMap(); + this._window.addEventListener('visibilitychange', + visibilityChangeHandler, + /* useCapture = */ false, + /* wantsUntrusted = */ false); + } + + this._window._browserElementParents.set(this, null); + + // Insert ourself into the prompt service. + BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this); + if (!isPendingFrame) { + this._setupMessageListener(); + this._registerAppManifest(); + } else { + // if we are a pending frame, we setup message manager after + // observing remote-browser-frame-shown + this._pendingAPICalls = []; + Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true); + } +} + +BrowserElementParent.prototype = { + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + _runPendingAPICall: function() { + if (!this._pendingAPICalls) { + return; + } + for (let i = 0; i < this._pendingAPICalls.length; i++) { + try { + this._pendingAPICalls[i](); + } catch (e) { + // throw the expections from pending functions. + debug('Exception when running pending API call: ' + e); + } + } + delete this._pendingAPICalls; + }, + + _registerAppManifest: function() { + // If this browser represents an app then let the Webapps module register for + // any messages that it needs. + let appManifestURL = + this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL; + if (appManifestURL) { + let appId = + DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL); + if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) { + DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId); + } + } + }, + + _setupMessageListener: function() { + this._mm = this._frameLoader.messageManager; + let self = this; + + // Messages we receive are handed to functions which take a (data) argument, + // where |data| is the message manager's data object. + // We use a single message and dispatch to various function based + // on data.msg_name + let mmCalls = { + "hello": this._recvHello, + "contextmenu": this._fireCtxMenuEvent, + "locationchange": this._fireEventFromMsg, + "loadstart": this._fireEventFromMsg, + "loadend": this._fireEventFromMsg, + "titlechange": this._fireEventFromMsg, + "iconchange": this._fireEventFromMsg, + "manifestchange": this._fireEventFromMsg, + "metachange": this._fireEventFromMsg, + "close": this._fireEventFromMsg, + "resize": this._fireEventFromMsg, + "activitydone": this._fireEventFromMsg, + "opensearch": this._fireEventFromMsg, + "securitychange": this._fireEventFromMsg, + "error": this._fireEventFromMsg, + "scroll": this._fireEventFromMsg, + "firstpaint": this._fireEventFromMsg, + "documentfirstpaint": this._fireEventFromMsg, + "nextpaint": this._recvNextPaint, + "keyevent": this._fireKeyEvent, + "showmodalprompt": this._handleShowModalPrompt, + "got-purge-history": this._gotDOMRequestResult, + "got-screenshot": this._gotDOMRequestResult, + "got-can-go-back": this._gotDOMRequestResult, + "got-can-go-forward": this._gotDOMRequestResult, + "fullscreen-origin-change": this._remoteFullscreenOriginChange, + "rollback-fullscreen": this._remoteFrameFullscreenReverted, + "exit-fullscreen": this._exitFullscreen, + "got-visible": this._gotDOMRequestResult, + "visibilitychange": this._childVisibilityChange, + "got-set-input-method-active": this._gotDOMRequestResult + }; + + this._mm.addMessageListener('browser-element-api:call', function(aMsg) { + if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) { + return mmCalls[aMsg.data.msg_name].apply(self, arguments); + } + }); + }, + + /** + * You shouldn't touch this._frameElement or this._window if _isAlive is + * false. (You'll likely get an exception if you do.) + */ + _isAlive: function() { + return !Cu.isDeadWrapper(this._frameElement) && + !Cu.isDeadWrapper(this._frameElement.ownerDocument) && + !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView); + }, + + get _window() { + return this._frameElement.ownerDocument.defaultView; + }, + + get _windowUtils() { + return this._window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }, + + promptAuth: function(authDetail, callback) { + let evt; + let self = this; + let callbackCalled = false; + let cancelCallback = function() { + if (!callbackCalled) { + callbackCalled = true; + callback(false, null, null); + } + }; + + if (authDetail.isOnlyPassword) { + // We don't handle password-only prompts, so just cancel it. + cancelCallback(); + return; + } else { /* username and password */ + let detail = { + host: authDetail.host, + realm: authDetail.realm + }; + + evt = this._createEvent('usernameandpasswordrequired', detail, + /* cancelable */ true); + defineAndExpose(evt.detail, 'authenticate', function(username, password) { + if (callbackCalled) + return; + callbackCalled = true; + callback(true, username, password); + }); + } + + defineAndExpose(evt.detail, 'cancel', function() { + cancelCallback(); + }); + + this._frameElement.dispatchEvent(evt); + + if (!evt.defaultPrevented) { + cancelCallback(); + } + }, + + _sendAsyncMsg: function(msg, data) { + try { + if (!data) { + data = { }; + } + + data.msg_name = msg; + this._mm.sendAsyncMessage('browser-element-api:call', data); + } catch (e) { + return false; + } + return true; + }, + + _recvHello: function() { + debug("recvHello"); + + this._ready = true; + + // Inform our child if our owner element's document is invisible. Note + // that we must do so here, rather than in the BrowserElementParent + // constructor, because the BrowserElementChild may not be initialized when + // we run our constructor. + if (this._window.document.hidden) { + this._ownerVisibilityChange(); + } + + return { + name: this._frameElement.getAttribute('name'), + fullscreenAllowed: + this._frameElement.hasAttribute('allowfullscreen') || + this._frameElement.hasAttribute('mozallowfullscreen') + }; + }, + + _fireCtxMenuEvent: function(data) { + let detail = data.json; + let evtName = detail.msg_name; + + debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail); + let evt = this._createEvent(evtName, detail, /* cancellable */ true); + + if (detail.contextmenu) { + var self = this; + defineAndExpose(evt.detail, 'contextMenuItemSelected', function(id) { + self._sendAsyncMsg('fire-ctx-callback', {menuitem: id}); + }); + } + + // The embedder may have default actions on context menu events, so + // we fire a context menu event even if the child didn't define a + // custom context menu + return !this._frameElement.dispatchEvent(evt); + }, + + /** + * Fire either a vanilla or a custom event, depending on the contents of + * |data|. + */ + _fireEventFromMsg: function(data) { + let detail = data.json; + let name = detail.msg_name; + + // For events that send a "_payload_" property, we just want to transmit + // this in the event. + if ("_payload_" in detail) { + detail = detail._payload_; + } + + debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail)); + let evt = this._createEvent(name, detail, + /* cancelable = */ false); + this._frameElement.dispatchEvent(evt); + }, + + _handleShowModalPrompt: function(data) { + // Fire a showmodalprmopt event on the iframe. When this method is called, + // the child is spinning in a nested event loop waiting for an + // unblock-modal-prompt message. + // + // If the embedder calls preventDefault() on the showmodalprompt event, + // we'll block the child until event.detail.unblock() is called. + // + // Otherwise, if preventDefault() is not called, we'll send the + // unblock-modal-prompt message to the child as soon as the event is done + // dispatching. + + let detail = data.json; + debug('handleShowPrompt ' + JSON.stringify(detail)); + + // Strip off the windowID property from the object we send along in the + // event. + let windowID = detail.windowID; + delete detail.windowID; + debug("Event will have detail: " + JSON.stringify(detail)); + let evt = this._createEvent('showmodalprompt', detail, + /* cancelable = */ true); + + let self = this; + let unblockMsgSent = false; + function sendUnblockMsg() { + if (unblockMsgSent) { + return; + } + unblockMsgSent = true; + + // We don't need to sanitize evt.detail.returnValue (e.g. converting the + // return value of confirm() to a boolean); Gecko does that for us. + + let data = { windowID: windowID, + returnValue: evt.detail.returnValue }; + self._sendAsyncMsg('unblock-modal-prompt', data); + } + + defineAndExpose(evt.detail, 'unblock', function() { + sendUnblockMsg(); + }); + + this._frameElement.dispatchEvent(evt); + + if (!evt.defaultPrevented) { + // Unblock the inner frame immediately. Otherwise we'll unblock upon + // evt.detail.unblock(). + sendUnblockMsg(); + } + }, + + _createEvent: function(evtName, detail, cancelable) { + // This will have to change if we ever want to send a CustomEvent with null + // detail. For now, it's OK. + if (detail !== undefined && detail !== null) { + exposeAll(detail); + return new this._window.CustomEvent('mozbrowser' + evtName, + { bubbles: true, + cancelable: cancelable, + detail: detail }); + } + + return new this._window.Event('mozbrowser' + evtName, + { bubbles: true, + cancelable: cancelable }); + }, + + /** + * If remote frame haven't been set up, we enqueue a function that get a + * DOMRequest until the remote frame is ready and return another DOMRequest + * to caller. When we get the real DOMRequest, we will help forward the + * success/error callback to the DOMRequest that caller got. + */ + _queueDOMRequest: function(msgName, args) { + if (!this._pendingAPICalls) { + return; + } + + let req = Services.DOMRequest.createRequest(this._window); + let self = this; + let getRealDOMRequest = function() { + let realReq = self._sendDOMRequest(msgName, args); + realReq.onsuccess = function(v) { + Services.DOMRequest.fireSuccess(req, v); + }; + realReq.onerror = function(v) { + Services.DOMRequest.fireError(req, v); + }; + }; + this._pendingAPICalls.push(getRealDOMRequest); + return req; + }, + + /** + * Kick off a DOMRequest in the child process. + * + * We'll fire an event called |msgName| on the child process, passing along + * an object with two fields: + * + * - id: the ID of this request. + * - arg: arguments to pass to the child along with this request. + * + * We expect the child to pass the ID back to us upon completion of the + * request. See _gotDOMRequestResult. + */ + _sendDOMRequest: function(msgName, args) { + let id = 'req_' + this._domRequestCounter++; + let req = Services.DOMRequest.createRequest(this._window); + if (this._sendAsyncMsg(msgName, {id: id, args: args})) { + this._pendingDOMRequests[id] = req; + } else { + Services.DOMRequest.fireErrorAsync(req, "fail"); + } + return req; + }, + + /** + * Called when the child process finishes handling a DOMRequest. data.json + * must have the fields [id, successRv], if the DOMRequest was successful, or + * [id, errorMsg], if the request was not successful. + * + * The fields have the following meanings: + * + * - id: the ID of the DOM request (see _sendDOMRequest) + * - successRv: the request's return value, if the request succeeded + * - errorMsg: the message to pass to DOMRequest.fireError(), if the request + * failed. + * + */ + _gotDOMRequestResult: function(data) { + let req = this._pendingDOMRequests[data.json.id]; + delete this._pendingDOMRequests[data.json.id]; + + if ('successRv' in data.json) { + debug("Successful gotDOMRequestResult."); + Services.DOMRequest.fireSuccess(req, data.json.successRv); + } + else { + debug("Got error in gotDOMRequestResult."); + Services.DOMRequest.fireErrorAsync(req, data.json.errorMsg); + } + }, + + _setVisible: function(visible) { + this._sendAsyncMsg('set-visible', {visible: visible}); + this._frameLoader.visible = visible; + }, + + _sendMouseEvent: function(type, x, y, button, clickCount, modifiers) { + this._sendAsyncMsg("send-mouse-event", { + "type": type, + "x": x, + "y": y, + "button": button, + "clickCount": clickCount, + "modifiers": modifiers + }); + }, + + _sendTouchEvent: function(type, identifiers, touchesX, touchesY, + radiisX, radiisY, rotationAngles, forces, + count, modifiers) { + + let tabParent = this._frameLoader.tabParent; + if (tabParent && tabParent.useAsyncPanZoom) { + tabParent.injectTouchEvent(type, + identifiers, + touchesX, + touchesY, + radiisX, + radiisY, + rotationAngles, + forces, + count, + modifiers); + } else { + this._sendAsyncMsg("send-touch-event", { + "type": type, + "identifiers": identifiers, + "touchesX": touchesX, + "touchesY": touchesY, + "radiisX": radiisX, + "radiisY": radiisY, + "rotationAngles": rotationAngles, + "forces": forces, + "count": count, + "modifiers": modifiers + }); + } + }, + + _goBack: function() { + this._sendAsyncMsg('go-back'); + }, + + _goForward: function() { + this._sendAsyncMsg('go-forward'); + }, + + _reload: function(hardReload) { + this._sendAsyncMsg('reload', {hardReload: hardReload}); + }, + + _stop: function() { + this._sendAsyncMsg('stop'); + }, + + _getScreenshot: function(_width, _height, _mimeType) { + let width = parseInt(_width); + let height = parseInt(_height); + let mimeType = (typeof _mimeType === 'string') ? + _mimeType.trim().toLowerCase() : 'image/jpeg'; + if (isNaN(width) || isNaN(height) || width < 0 || height < 0) { + throw Components.Exception("Invalid argument", + Cr.NS_ERROR_INVALID_ARG); + } + + if (!this._mm) { + // Child haven't been loaded. + return this._queueDOMRequest('get-screenshot', + {width: width, height: height, + mimeType: mimeType}); + } + + return this._sendDOMRequest('get-screenshot', + {width: width, height: height, + mimeType: mimeType}); + }, + + _recvNextPaint: function(data) { + let listeners = this._nextPaintListeners; + this._nextPaintListeners = []; + for (let listener of listeners) { + try { + listener(); + } catch (e) { + // If a listener throws we'll continue. + } + } + }, + + _addNextPaintListener: function(listener) { + if (typeof listener != 'function') + throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); + + let self = this; + let run = function() { + if (self._nextPaintListeners.push(listener) == 1) + self._sendAsyncMsg('activate-next-paint-listener'); + }; + if (!this._mm) { + this._pendingAPICalls.push(run); + } else { + run(); + } + }, + + _removeNextPaintListener: function(listener) { + if (typeof listener != 'function') + throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); + + let self = this; + let run = function() { + for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) { + if (self._nextPaintListeners[i] == listener) { + self._nextPaintListeners.splice(i, 1); + break; + } + } + + if (self._nextPaintListeners.length == 0) + self._sendAsyncMsg('deactivate-next-paint-listener'); + }; + if (!this._mm) { + this._pendingAPICalls.push(run); + } else { + run(); + } + }, + + _setInputMethodActive: function(isActive) { + if (typeof isActive !== 'boolean') { + throw Components.Exception("Invalid argument", + Cr.NS_ERROR_INVALID_ARG); + } + + let req = Services.DOMRequest.createRequest(this._window); + + // Deactivate the old input method if needed. + if (activeInputFrame && isActive) { + if (Cu.isDeadWrapper(activeInputFrame)) { + // If the activeInputFrame is already a dead object, + // we should simply set it to null directly. + activeInputFrame = null; + this._sendSetInputMethodActiveDOMRequest(req, isActive); + } else { + let reqOld = XPCNativeWrapper.unwrap(activeInputFrame) + .setInputMethodActive(false); + + // We wan't to continue regardless whether this req succeeded + reqOld.onsuccess = reqOld.onerror = function() { + let setActive = function() { + activeInputFrame = null; + this._sendSetInputMethodActiveDOMRequest(req, isActive); + }.bind(this); + + if (this._ready) { + setActive(); + return; + } + + // Wait for the hello event from BrowserElementChild + let onReady = function(aMsg) { + if (this._isAlive() && (aMsg.data.msg_name === 'hello')) { + setActive(); + + this._mm.removeMessageListener('browser-element-api:call', + onReady); + } + }.bind(this); + + this._mm.addMessageListener('browser-element-api:call', onReady); + }.bind(this); + } + } else { + this._sendSetInputMethodActiveDOMRequest(req, isActive); + } + return req; + }, + + _sendSetInputMethodActiveDOMRequest: function(req, isActive) { + let id = 'req_' + this._domRequestCounter++; + let data = { + id : id, + args: { isActive: isActive } + }; + if (this._sendAsyncMsg('set-input-method-active', data)) { + activeInputFrame = this._frameElement; + this._pendingDOMRequests[id] = req; + } else { + Services.DOMRequest.fireErrorAsync(req, 'fail'); + } + }, + + _fireKeyEvent: function(data) { + let evt = this._window.document.createEvent("KeyboardEvent"); + evt.initKeyEvent(data.json.type, true, true, this._window, + false, false, false, false, // modifiers + data.json.keyCode, + data.json.charCode); + + this._frameElement.dispatchEvent(evt); + }, + + /** + * Called when the visibility of the window which owns this iframe changes. + */ + _ownerVisibilityChange: function() { + this._sendAsyncMsg('owner-visibility-change', + {visible: !this._window.document.hidden}); + }, + + /* + * Called when the child notices that its visibility has changed. + * + * This is sometimes redundant; for example, the child's visibility may + * change in response to a setVisible request that we made here! But it's + * not always redundant; for example, the child's visibility may change in + * response to its parent docshell being hidden. + */ + _childVisibilityChange: function(data) { + debug("_childVisibilityChange(" + data.json.visible + ")"); + this._frameLoader.visible = data.json.visible; + + this._fireEventFromMsg(data); + }, + + _exitFullscreen: function() { + this._windowUtils.exitFullscreen(); + }, + + _remoteFullscreenOriginChange: function(data) { + let origin = data.json._payload_; + this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin); + }, + + _remoteFrameFullscreenReverted: function(data) { + this._windowUtils.remoteFrameFullscreenReverted(); + }, + + _fireFatalError: function() { + let evt = this._createEvent('error', {type: 'fatal'}, + /* cancelable = */ false); + this._frameElement.dispatchEvent(evt); + }, + + observe: function(subject, topic, data) { + switch(topic) { + case 'oop-frameloader-crashed': + if (this._isAlive() && subject == this._frameLoader) { + this._fireFatalError(); + } + break; + case 'ask-children-to-exit-fullscreen': + if (this._isAlive() && + this._frameElement.ownerDocument == subject && + this._hasRemoteFrame) { + this._sendAsyncMsg('exit-fullscreen'); + } + break; + case 'remote-browser-frame-shown': + if (this._frameLoader == subject) { + if (!this._mm) { + this._setupMessageListener(); + this._registerAppManifest(); + this._runPendingAPICall(); + } + Services.obs.removeObserver(this, 'remote-browser-frame-shown'); + } + default: + debug('Unknown topic: ' + topic); + break; + }; + }, +};