gecko-dev/browser/actors/DOMFullscreenParent.sys.mjs
Brad Werth 80e8eb7818 Bug 1826645 Part 2: Make Document hold fullscreen requests while an exit is being processed. r=edgar
This change makes the parent process delay a fullscreen request if there
is a pending fullscreen exit. It also changes the DOMFullscreenParent
actor listener lifecycle. Once it has started handling a fullscreen
request, it will remain a listener to the Document until it receives an
exit event when the manager is out of fullscreen.

Differential Revision: https://phabricator.services.mozilla.com/D175186
2023-04-18 16:10:03 +00:00

319 lines
11 KiB
JavaScript

/* vim: set ts=2 sw=2 sts=2 et tw=80: */
/* 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/. */
export class DOMFullscreenParent extends JSWindowActorParent {
// These properties get set by browser-fullScreenAndPointerLock.js.
// TODO: Bug 1743703 - Consider moving the messaging component of
// browser-fullScreenAndPointerLock.js into the actor
waitingForChildEnterFullscreen = false;
waitingForChildExitFullscreen = false;
// Cache the next message recipient actor and in-process browsing context that
// is computed by _getNextMsgRecipientActor() of
// browser-fullScreenAndPointerLock.js, this is used to ensure the fullscreen
// cleanup messages goes the same route as fullscreen request, especially for
// the cleanup that happens after actor is destroyed.
// TODO: Bug 1743703 - Consider moving the messaging component of
// browser-fullScreenAndPointerLock.js into the actor
nextMsgRecipient = null;
updateFullscreenWindowReference(aWindow) {
if (aWindow.document.documentElement.hasAttribute("inDOMFullscreen")) {
this._fullscreenWindow = aWindow;
} else {
delete this._fullscreenWindow;
}
}
cleanupDomFullscreen(aWindow) {
if (!aWindow.FullScreen) {
return;
}
// If we don't need to wait for child reply, i.e. cleanupDomFullscreen
// doesn't message to child, and we've exit the fullscreen, there won't be
// DOMFullscreen:Painted message from child and it is possible that no more
// paint would be triggered, so just notify fullscreen-painted observer.
if (
!aWindow.FullScreen.cleanupDomFullscreen(this) &&
!aWindow.document.fullscreen
) {
Services.obs.notifyObservers(aWindow, "fullscreen-painted");
}
}
/**
* Clean up fullscreen state and resume chrome UI if window is in fullscreen
* and this actor is the one where the original fullscreen enter or
* exit request comes.
*/
_cleanupFullscreenStateAndResumeChromeUI(aWindow) {
this.cleanupDomFullscreen(aWindow);
if (this.requestOrigin == this && aWindow.document.fullscreen) {
aWindow.windowUtils.remoteFrameFullscreenReverted();
}
}
didDestroy() {
this._didDestroy = true;
let window = this._fullscreenWindow;
if (!window) {
let topBrowsingContext = this.browsingContext.top;
let browser = topBrowsingContext.embedderElement;
if (!browser) {
return;
}
if (
this.waitingForChildExitFullscreen ||
this.waitingForChildEnterFullscreen
) {
this.waitingForChildExitFullscreen = false;
this.waitingForChildEnterFullscreen = false;
// We were destroyed while waiting for our DOMFullscreenChild to exit
// or enter fullscreen, run cleanup steps anyway.
this._cleanupFullscreenStateAndResumeChromeUI(browser.ownerGlobal);
}
if (this != this.requestOrigin) {
// The current fullscreen requester should handle the fullsceen event
// if any.
this.removeListeners(browser.ownerGlobal);
}
return;
}
if (this.waitingForChildEnterFullscreen) {
this.waitingForChildEnterFullscreen = false;
if (window.document.fullscreen) {
// We were destroyed while waiting for our DOMFullscreenChild
// to transition to fullscreen so we abort the entire
// fullscreen transition to prevent getting stuck in a
// partial fullscreen state. We need to go through the
// document since window.Fullscreen could be undefined
// at this point.
//
// This could reject if we're not currently in fullscreen
// so just ignore rejection.
window.document.exitFullscreen().catch(() => {});
return;
}
this.cleanupDomFullscreen(window);
}
// Need to resume Chrome UI if the window is still in fullscreen UI
// to avoid the window stays in fullscreen problem. (See Bug 1620341)
if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
this.cleanupDomFullscreen(window);
if (window.windowUtils) {
window.windowUtils.remoteFrameFullscreenReverted();
}
} else if (this.waitingForChildExitFullscreen) {
this.waitingForChildExitFullscreen = false;
// We were destroyed while waiting for our DOMFullscreenChild to exit
// run cleanup steps anyway.
this._cleanupFullscreenStateAndResumeChromeUI(window);
}
this.updateFullscreenWindowReference(window);
}
receiveMessage(aMessage) {
let topBrowsingContext = this.browsingContext.top;
let browser = topBrowsingContext.embedderElement;
if (!browser) {
// No need to go further when the browser is not accessible anymore
// (which can happen when the tab is closed for instance),
return;
}
let window = browser.ownerGlobal;
switch (aMessage.name) {
case "DOMFullscreen:Request": {
this.manager.fullscreen = true;
this.waitingForChildExitFullscreen = false;
this.requestOrigin = this;
this.addListeners(window);
window.windowUtils.remoteFrameFullscreenChanged(browser);
break;
}
case "DOMFullscreen:NewOrigin": {
// Don't show the warning if we've already exited fullscreen.
if (window.document.fullscreen) {
window.PointerlockFsWarning.showFullScreen(
aMessage.data.originNoSuffix
);
}
this.updateFullscreenWindowReference(window);
break;
}
case "DOMFullscreen:Entered": {
this.manager.fullscreen = true;
this.nextMsgRecipient = null;
this.waitingForChildEnterFullscreen = false;
window.FullScreen.enterDomFullscreen(browser, this);
this.updateFullscreenWindowReference(window);
break;
}
case "DOMFullscreen:Exit": {
this.manager.fullscreen = false;
this.waitingForChildEnterFullscreen = false;
window.windowUtils.remoteFrameFullscreenReverted();
break;
}
case "DOMFullscreen:Exited": {
this.manager.fullscreen = false;
this.waitingForChildExitFullscreen = false;
this.cleanupDomFullscreen(window);
this.updateFullscreenWindowReference(window);
break;
}
case "DOMFullscreen:Painted": {
this.waitingForChildExitFullscreen = false;
Services.obs.notifyObservers(window, "fullscreen-painted");
this.sendAsyncMessage("DOMFullscreen:Painted", {});
TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS");
break;
}
}
}
handleEvent(aEvent) {
let window = aEvent.currentTarget.ownerGlobal;
// We can not get the corresponding browsing context from actor if the actor
// has already destroyed, so use event target to get browsing context
// instead.
let requestOrigin = window.browsingContext.fullscreenRequestOrigin?.get();
if (this != requestOrigin) {
// The current fullscreen requester should handle the fullsceen event,
// ignore them if we are not the current requester.
this.removeListeners(window);
return;
}
switch (aEvent.type) {
case "MozDOMFullscreen:Entered": {
// The event target is the element which requested the DOM
// fullscreen. If we were entering DOM fullscreen for a remote
// browser, the target would be the browser which was the parameter of
// `remoteFrameFullscreenChanged` call. If the fullscreen
// request was initiated from an in-process browser, we need
// to get its corresponding browser here.
let browser;
if (aEvent.target.ownerGlobal == window) {
browser = aEvent.target;
} else {
browser = aEvent.target.ownerGlobal.docShell.chromeEventHandler;
}
// Addon installation should be cancelled when entering fullscreen for security and usability reasons.
// Installation prompts in fullscreen can trick the user into installing unwanted addons.
// In fullscreen the notification box does not have a clear visual association with its parent anymore.
if (window.gXPInstallObserver) {
window.gXPInstallObserver.removeAllNotifications(browser);
}
TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
window.FullScreen.enterDomFullscreen(browser, this);
this.updateFullscreenWindowReference(window);
if (!this.hasBeenDestroyed() && this.requestOrigin) {
window.PointerlockFsWarning.showFullScreen(
this.requestOrigin.manager.documentPrincipal.originNoSuffix
);
}
break;
}
case "MozDOMFullscreen:Exited": {
TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
// Make sure that the actor has not been destroyed before
// accessing its browsing context. Otherwise, a error may
// occur and hence cleanupDomFullscreen not executed, resulting
// in the browser window being in an unstable state.
// (Bug 1590138).
if (!this.hasBeenDestroyed() && !this.requestOrigin) {
this.requestOrigin = this;
}
this.cleanupDomFullscreen(window);
this.updateFullscreenWindowReference(window);
// If the document is supposed to be in fullscreen, keep the listener to wait for
// further events.
if (!this.manager.fullscreen) {
this.removeListeners(window);
}
break;
}
}
}
addListeners(aWindow) {
aWindow.addEventListener(
"MozDOMFullscreen:Entered",
this,
/* useCapture */ true,
/* wantsUntrusted */
false
);
aWindow.addEventListener(
"MozDOMFullscreen:Exited",
this,
/* useCapture */ true,
/* wantsUntrusted */ false
);
}
removeListeners(aWindow) {
aWindow.removeEventListener("MozDOMFullscreen:Entered", this, true);
aWindow.removeEventListener("MozDOMFullscreen:Exited", this, true);
}
/**
* Get the actor where the original fullscreen
* enter or exit request comes from.
*/
get requestOrigin() {
let chromeBC = this.browsingContext.topChromeWindow?.browsingContext;
let requestOrigin = chromeBC?.fullscreenRequestOrigin;
return requestOrigin && requestOrigin.get();
}
/**
* Store the actor where the original fullscreen
* enter or exit request comes from in the top level
* browsing context.
*/
set requestOrigin(aActor) {
let chromeBC = this.browsingContext.topChromeWindow?.browsingContext;
if (!chromeBC) {
console.error("not able to get browsingContext for chrome window.");
return;
}
if (aActor) {
chromeBC.fullscreenRequestOrigin = Cu.getWeakReference(aActor);
} else {
delete chromeBC.fullscreenRequestOrigin;
}
}
hasBeenDestroyed() {
if (this._didDestroy) {
return true;
}
// The 'didDestroy' callback is not always getting called.
// So we can't rely on it here. Instead, we will try to access
// the browsing context to judge wether the actor has
// been destroyed or not.
try {
return !this.browsingContext;
} catch {
return true;
}
}
}