From 4c8aa34ba2fc7d8efd5d186d6d19a98cc4b4dc13 Mon Sep 17 00:00:00 2001 From: Agi Sferro Date: Thu, 13 Aug 2020 20:24:54 +0000 Subject: [PATCH] Bug 1648149 - Move GeckoViewContent to Actor. r=snorp Differential Revision: https://phabricator.services.mozilla.com/D84040 --- layout/forms/test/mochitest.ini | 1 + .../android/actors/GeckoViewContentChild.jsm | 430 ++++++++++++++- .../android/actors/GeckoViewContentParent.jsm | 33 ++ mobile/android/actors/moz.build | 1 + .../chrome/geckoview/GeckoViewContentChild.js | 498 ------------------ .../chrome/geckoview/extension-content.js | 12 + mobile/android/chrome/geckoview/geckoview.js | 9 +- mobile/android/chrome/geckoview/jar.mn | 2 +- .../components/geckoview/GeckoViewStartup.jsm | 17 + .../modules/geckoview/GeckoViewContent.jsm | 87 +-- .../modules/geckoview/GeckoViewModule.jsm | 4 + .../close-method.window.js.ini | 6 +- .../self-et-al.window.js.ini | 14 +- 13 files changed, 554 insertions(+), 560 deletions(-) create mode 100644 mobile/android/actors/GeckoViewContentParent.jsm delete mode 100644 mobile/android/chrome/geckoview/GeckoViewContentChild.js create mode 100644 mobile/android/chrome/geckoview/extension-content.js diff --git a/layout/forms/test/mochitest.ini b/layout/forms/test/mochitest.ini index 88a5a9a99194..9d9bcae530bc 100644 --- a/layout/forms/test/mochitest.ini +++ b/layout/forms/test/mochitest.ini @@ -20,6 +20,7 @@ skip-if = toolkit == 'android' || e10s || os == 'mac' # mac(select form control [test_bug477531.html] [test_bug477700.html] [test_bug478219.xhtml] +skip-if = toolkit == 'android' # Bug 1658807 [test_bug534785.html] [test_bug542914.html] [test_bug549170.html] diff --git a/mobile/android/actors/GeckoViewContentChild.jsm b/mobile/android/actors/GeckoViewContentChild.jsm index 5bcb862f8a40..8b1f57733f3c 100644 --- a/mobile/android/actors/GeckoViewContentChild.jsm +++ b/mobile/android/actors/GeckoViewContentChild.jsm @@ -10,14 +10,81 @@ var { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// This needs to match ScreenLength.java +const SCREEN_LENGTH_TYPE_PIXEL = 0; +const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH = 1; +const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT = 2; +const SCREEN_LENGTH_DOCUMENT_WIDTH = 3; +const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4; + +// This need to match PanZoomController.java +const SCROLL_BEHAVIOR_SMOOTH = 0; +const SCROLL_BEHAVIOR_AUTO = 1; + XPCOMUtils.defineLazyModuleGetters(this, { - SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm", + E10SUtils: "resource://gre/modules/E10SUtils.jsm", + ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm", PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm", + SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm", + Utils: "resource://gre/modules/sessionstore/Utils.jsm", }); var EXPORTED_SYMBOLS = ["GeckoViewContentChild"]; class GeckoViewContentChild extends GeckoViewActorChild { + toPixels(aLength, aType) { + const { contentWindow } = this; + if (aType === SCREEN_LENGTH_TYPE_PIXEL) { + return aLength; + } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) { + return aLength * contentWindow.visualViewport.width; + } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) { + return aLength * contentWindow.visualViewport.height; + } else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) { + return aLength * contentWindow.document.body.scrollWidth; + } else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) { + return aLength * contentWindow.document.body.scrollHeight; + } + + return aLength; + } + + toScrollBehavior(aBehavior) { + const { contentWindow } = this; + if (!contentWindow) { + return 0; + } + const { windowUtils } = contentWindow; + if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) { + return windowUtils.SCROLL_MODE_SMOOTH; + } else if (aBehavior === SCROLL_BEHAVIOR_AUTO) { + return windowUtils.SCROLL_MODE_INSTANT; + } + return windowUtils.SCROLL_MODE_SMOOTH; + } + + notifyParentOfViewportFit() { + if (this.triggerViewportFitChange) { + this.contentWindow.cancelIdleCallback(this.triggerViewportFitChange); + } + this.triggerViewportFitChange = this.contentWindow.requestIdleCallback( + () => { + this.triggerViewportFitChange = null; + const viewportFit = this.contentWindow.windowUtils.getViewportFitInfo(); + if (this.lastViewportFit === viewportFit) { + return; + } + this.lastViewportFit = viewportFit; + this.eventDispatcher.sendRequest({ + type: "GeckoView:DOMMetaViewportFit", + viewportfit: viewportFit, + }); + } + ); + } + collectSessionState() { const { docShell, contentWindow } = this; const history = SessionHistory.collect(docShell); @@ -49,14 +116,371 @@ class GeckoViewContentChild extends GeckoViewActorChild { return { history, formdata, scrolldata }; } + loadEntry(loadOptions, history) { + if (!loadOptions) { + history.QueryInterface(Ci.nsISHistory).reloadCurrentEntry(); + return; + } + + const webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); + + const { + referrerInfo, + triggeringPrincipal, + uri, + flags, + csp, + headers, + } = loadOptions; + + webNavigation.loadURI(uri, { + triggeringPrincipal: E10SUtils.deserializePrincipal(triggeringPrincipal), + referrerInfo: E10SUtils.deserializeReferrerInfo(referrerInfo), + loadFlags: flags, + csp: E10SUtils.deserializeCSP(csp), + headers, + }); + } + receiveMessage(message) { const { name } = message; + debug`receiveMessage: ${name}`; + switch (name) { + case "GeckoView:DOMFullscreenEntered": + this.contentWindow?.windowUtils.handleFullscreenRequests(); + break; + case "GeckoView:DOMFullscreenExited": + this.contentWindow?.windowUtils.exitFullscreen(); + break; + case "GeckoView:ZoomToInput": { + const { contentWindow } = this; + const dwu = contentWindow.windowUtils; + + const zoomToFocusedInput = function() { + if (!dwu.flushApzRepaints()) { + dwu.zoomToFocusedInput(); + return; + } + Services.obs.addObserver(function apzFlushDone() { + Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed"); + dwu.zoomToFocusedInput(); + }, "apz-repaints-flushed"); + }; + + const { force } = message.data; + + let gotResize = false; + const onResize = function() { + gotResize = true; + if (dwu.isMozAfterPaintPending) { + contentWindow.addEventListener( + "MozAfterPaint", + () => zoomToFocusedInput(), + { capture: true, once: true } + ); + } else { + zoomToFocusedInput(); + } + }; + + contentWindow.addEventListener("resize", onResize, { capture: true }); + + // When the keyboard is displayed, we can get one resize event, + // multiple resize events, or none at all. Try to handle all these + // cases by allowing resizing within a set interval, and still zoom to + // input if there is no resize event at the end of the interval. + contentWindow.setTimeout(() => { + contentWindow.removeEventListener("resize", onResize, { + capture: true, + }); + if (!gotResize && force) { + onResize(); + } + }, 500); + break; + } + case "GeckoView:RestoreState": { + const { contentWindow, docShell } = this; + const { history, formdata, scrolldata, loadOptions } = message.data; + + if (history) { + const restoredHistory = SessionHistory.restore(docShell, history); + + contentWindow.addEventListener( + "load", + _ => { + if (formdata) { + Utils.restoreFrameTreeData( + contentWindow, + formdata, + (frame, data) => { + // restore() will return false, and thus abort restoration for the + // current |frame| and its descendants, if |data.url| is given but + // doesn't match the loaded document's URL. + return SessionStoreUtils.restoreFormData( + frame.document, + data + ); + } + ); + } + }, + { capture: true, mozSystemGroup: true, once: true } + ); + + const scrollRestore = _ => { + if (contentWindow.location != "about:blank") { + if (scrolldata) { + Utils.restoreFrameTreeData( + contentWindow, + scrolldata, + (frame, data) => { + if (data.scroll) { + SessionStoreUtils.restoreScrollPosition(frame, data); + } + } + ); + } + contentWindow.removeEventListener("pageshow", scrollRestore, { + capture: true, + mozSystemGroup: true, + }); + } + }; + + contentWindow.addEventListener("pageshow", scrollRestore, { + capture: true, + mozSystemGroup: true, + }); + + const progressFilter = Cc[ + "@mozilla.org/appshell/component/browser-status-filter;1" + ].createInstance(Ci.nsIWebProgress); + const flags = Ci.nsIWebProgress.NOTIFY_LOCATION; + + const progressListener = { + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener"]), + + onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + debug`onLocationChange`; + + if ( + scrolldata && + scrolldata.zoom && + scrolldata.zoom.displaySize + ) { + const utils = contentWindow.windowUtils; + // Restore zoom level. + utils.setRestoreResolution( + scrolldata.zoom.resolution, + scrolldata.zoom.displaySize.width, + scrolldata.zoom.displaySize.height + ); + } + + progressFilter.removeProgressListener(this); + const webProgress = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(progressFilter); + }, + }; + + progressFilter.addProgressListener(progressListener, flags); + const webProgress = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(progressFilter, flags); + + this.loadEntry(loadOptions, restoredHistory); + } + break; + } + case "GeckoView:UpdateInitData": { + // Provide a hook for native code to detect a transfer. + Services.obs.notifyObservers( + this.docShell, + "geckoview-content-global-transferred" + ); + break; + } + case "GeckoView:ScrollBy": { + const x = {}; + const y = {}; + const { contentWindow } = this; + const { + widthValue, + widthType, + heightValue, + heightType, + behavior, + } = message.data; + contentWindow.windowUtils.getVisualViewportOffset(x, y); + contentWindow.windowUtils.scrollToVisual( + x.value + this.toPixels(widthValue, widthType), + y.value + this.toPixels(heightValue, heightType), + contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD, + this.toScrollBehavior(behavior) + ); + break; + } + case "GeckoView:ScrollTo": { + const { contentWindow } = this; + const { + widthValue, + widthType, + heightValue, + heightType, + behavior, + } = message.data; + contentWindow.windowUtils.scrollToVisual( + this.toPixels(widthValue, widthType), + this.toPixels(heightValue, heightType), + contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD, + this.toScrollBehavior(behavior) + ); + break; + } case "CollectSessionState": { return this.collectSessionState(); } - default: - return null; + } + + return null; + } + + // eslint-disable-next-line complexity + handleEvent(aEvent) { + debug`handleEvent: ${aEvent.type}`; + + switch (aEvent.type) { + case "contextmenu": { + function nearestParentAttribute(aNode, aAttribute) { + while ( + aNode && + aNode.hasAttribute && + !aNode.hasAttribute(aAttribute) + ) { + aNode = aNode.parentNode; + } + return aNode && aNode.getAttribute && aNode.getAttribute(aAttribute); + } + + function createAbsoluteUri(aBaseUri, aUri) { + if (!aUri || !aBaseUri || !aBaseUri.displaySpec) { + return null; + } + return Services.io.newURI(aUri, null, aBaseUri).displaySpec; + } + + const node = aEvent.composedTarget; + const baseUri = node.ownerDocument.baseURIObject; + const uri = createAbsoluteUri( + baseUri, + nearestParentAttribute(node, "href") + ); + const title = nearestParentAttribute(node, "title"); + const alt = nearestParentAttribute(node, "alt"); + const elementType = ChromeUtils.getClassName(node); + const isImage = elementType === "HTMLImageElement"; + const isMedia = + elementType === "HTMLVideoElement" || + elementType === "HTMLAudioElement"; + const elementSrc = + (isImage || isMedia) && (node.currentSrc || node.src); + + if (uri || isImage || isMedia) { + const msg = { + type: "GeckoView:ContextMenu", + screenX: aEvent.screenX, + screenY: aEvent.screenY, + baseUri: (baseUri && baseUri.displaySpec) || null, + uri, + title, + alt, + elementType, + elementSrc: elementSrc || null, + }; + + this.eventDispatcher.sendRequest(msg); + aEvent.preventDefault(); + } + break; + } + case "MozDOMFullscreen:Request": { + this.sendAsyncMessage("GeckoView:DOMFullscreenRequest", {}); + break; + } + case "MozDOMFullscreen:Entered": + case "MozDOMFullscreen:Exited": + // Content may change fullscreen state by itself, and we should ensure + // that the parent always exits fullscreen when content has left + // full screen mode. + if (this.contentWindow?.document.fullscreenElement) { + break; + } + // fall-through + case "MozDOMFullscreen:Exit": + this.sendAsyncMessage("GeckoView:DOMFullscreenExit", {}); + break; + case "DOMMetaViewportFitChanged": + if (aEvent.originalTarget.ownerGlobal == this.contentWindow) { + this.notifyParentOfViewportFit(); + } + break; + case "DOMTitleChanged": + this.eventDispatcher.sendRequest({ + type: "GeckoView:DOMTitleChanged", + title: this.contentWindow.document.title, + }); + break; + case "DOMWindowClose": + if (!aEvent.isTrusted) { + return; + } + + aEvent.preventDefault(); + this.eventDispatcher.sendRequest({ + type: "GeckoView:DOMWindowClose", + }); + break; + case "mozcaretstatechanged": + if ( + aEvent.reason === "presscaret" || + aEvent.reason === "releasecaret" + ) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:PinOnScreen", + pinned: aEvent.reason === "presscaret", + }); + } + break; + case "DOMContentLoaded": { + if (aEvent.originalTarget.ownerGlobal == this.contentWindow) { + // If loaded content doesn't have viewport-fit, parent still + // uses old value of previous content. + this.notifyParentOfViewportFit(); + } + this.contentWindow.requestIdleCallback(async () => { + const manifest = await ManifestObtainer.contentObtainManifest( + this.contentWindow + ); + if (manifest) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:WebAppManifest", + manifest, + }); + } + }); + break; + } + case "MozFirstContentfulPaint": { + this.eventDispatcher.sendRequest({ + type: "GeckoView:FirstContentfulPaint", + }); + break; + } } } } diff --git a/mobile/android/actors/GeckoViewContentParent.jsm b/mobile/android/actors/GeckoViewContentParent.jsm new file mode 100644 index 000000000000..1961ef57b2a1 --- /dev/null +++ b/mobile/android/actors/GeckoViewContentParent.jsm @@ -0,0 +1,33 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["GeckoViewContentParent"]; + +const { GeckoViewUtils } = ChromeUtils.import( + "resource://gre/modules/GeckoViewUtils.jsm" +); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewContentParent"); // eslint-disable-line no-unused-vars + +class GeckoViewContentParent extends JSWindowActorParent { + async receiveMessage(aMsg) { + debug`receiveMessage: ${aMsg.name} ${aMsg}`; + + const browser = this.browsingContext.top.embedderElement; + const window = browser.ownerGlobal; + + switch (aMsg.name) { + case "GeckoView:DOMFullscreenExit": { + window.windowUtils.remoteFrameFullscreenReverted(); + break; + } + + case "GeckoView:DOMFullscreenRequest": { + window.windowUtils.remoteFrameFullscreenChanged(browser); + break; + } + } + } +} diff --git a/mobile/android/actors/moz.build b/mobile/android/actors/moz.build index 2156c82010f9..546742a65b92 100644 --- a/mobile/android/actors/moz.build +++ b/mobile/android/actors/moz.build @@ -8,6 +8,7 @@ with Files('**'): FINAL_TARGET_FILES.actors += [ 'BrowserTabParent.jsm', 'GeckoViewContentChild.jsm', + 'GeckoViewContentParent.jsm', 'LoadURIDelegateChild.jsm', 'WebBrowserChromeChild.jsm', ] diff --git a/mobile/android/chrome/geckoview/GeckoViewContentChild.js b/mobile/android/chrome/geckoview/GeckoViewContentChild.js deleted file mode 100644 index dac1d18a818b..000000000000 --- a/mobile/android/chrome/geckoview/GeckoViewContentChild.js +++ /dev/null @@ -1,498 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* 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/. */ - -const { GeckoViewChildModule } = ChromeUtils.import( - "resource://gre/modules/GeckoViewChildModule.jsm" -); -var { XPCOMUtils } = ChromeUtils.import( - "resource://gre/modules/XPCOMUtils.jsm" -); -var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -// This needs to match ScreenLength.java -const SCREEN_LENGTH_TYPE_PIXEL = 0; -const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH = 1; -const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT = 2; -const SCREEN_LENGTH_DOCUMENT_WIDTH = 3; -const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4; - -// This need to match PanZoomController.java -const SCROLL_BEHAVIOR_SMOOTH = 0; -const SCROLL_BEHAVIOR_AUTO = 1; - -XPCOMUtils.defineLazyModuleGetters(this, { - E10SUtils: "resource://gre/modules/E10SUtils.jsm", - ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm", - PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.jsm", - SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm", -}); - -class GeckoViewContentChild extends GeckoViewChildModule { - onInit() { - debug`onInit`; - - // We don't load this in the global namespace because - // a Utils.jsm in a11y will clobber us. - XPCOMUtils.defineLazyModuleGetters(this, { - Utils: "resource://gre/modules/sessionstore/Utils.jsm", - }); - - this.timeoutsSuspended = false; - this.lastViewportFit = ""; - this.triggerViewportFitChange = null; - - this.messageManager.addMessageListener( - "GeckoView:DOMFullscreenEntered", - this - ); - this.messageManager.addMessageListener( - "GeckoView:DOMFullscreenExited", - this - ); - this.messageManager.addMessageListener("GeckoView:RestoreState", this); - this.messageManager.addMessageListener("GeckoView:UpdateInitData", this); - this.messageManager.addMessageListener("GeckoView:ZoomToInput", this); - this.messageManager.addMessageListener("GeckoView:ScrollBy", this); - this.messageManager.addMessageListener("GeckoView:ScrollTo", this); - - const options = { - mozSystemGroup: true, - capture: false, - }; - addEventListener("mozcaretstatechanged", this, options); - - // Notify WebExtension process script that this tab is ready for extension content to load. - Services.obs.notifyObservers( - this.messageManager, - "tab-content-frameloader-created" - ); - } - - onEnable() { - debug`onEnable`; - - addEventListener("DOMTitleChanged", this, false); - addEventListener("DOMWindowClose", this, false); - addEventListener("MozDOMFullscreen:Entered", this, false); - addEventListener("MozDOMFullscreen:Exit", this, false); - addEventListener("MozDOMFullscreen:Exited", this, false); - addEventListener("MozDOMFullscreen:Request", this, false); - addEventListener("contextmenu", this, { capture: true }); - addEventListener("DOMContentLoaded", this, false); - addEventListener("MozFirstContentfulPaint", this, false); - addEventListener("DOMMetaViewportFitChanged", this, false); - } - - onDisable() { - debug`onDisable`; - - removeEventListener("DOMTitleChanged", this); - removeEventListener("DOMWindowClose", this); - removeEventListener("MozDOMFullscreen:Entered", this); - removeEventListener("MozDOMFullscreen:Exit", this); - removeEventListener("MozDOMFullscreen:Exited", this); - removeEventListener("MozDOMFullscreen:Request", this); - removeEventListener("contextmenu", this, { capture: true }); - removeEventListener("DOMContentLoaded", this); - removeEventListener("MozFirstContentfulPaint", this); - removeEventListener("DOMMetaViewportFitChanged", this); - } - - toPixels(aLength, aType) { - if (aType === SCREEN_LENGTH_TYPE_PIXEL) { - return aLength; - } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) { - return aLength * content.visualViewport.width; - } else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) { - return aLength * content.visualViewport.height; - } else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) { - return aLength * content.document.body.scrollWidth; - } else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) { - return aLength * content.document.body.scrollHeight; - } - - return aLength; - } - - toScrollBehavior(aBehavior) { - if (!content) { - return 0; - } - if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) { - return content.windowUtils.SCROLL_MODE_SMOOTH; - } else if (aBehavior === SCROLL_BEHAVIOR_AUTO) { - return content.windowUtils.SCROLL_MODE_INSTANT; - } - return content.windowUtils.SCROLL_MODE_SMOOTH; - } - - notifyParentOfViewportFit() { - if (this.triggerViewportFitChange) { - content.cancelIdleCallback(this.triggerViewportFitChange); - } - this.triggerViewportFitChange = content.requestIdleCallback(() => { - this.triggerViewportFitChange = null; - const viewportFit = content.windowUtils.getViewportFitInfo(); - if (this.lastViewportFit === viewportFit) { - return; - } - this.lastViewportFit = viewportFit; - this.eventDispatcher.sendRequest({ - type: "GeckoView:DOMMetaViewportFit", - viewportfit: viewportFit, - }); - }); - } - - receiveMessage(aMsg) { - debug`receiveMessage: ${aMsg.name}`; - - switch (aMsg.name) { - case "GeckoView:DOMFullscreenEntered": - if (content) { - content.windowUtils.handleFullscreenRequests(); - } - break; - - case "GeckoView:DOMFullscreenExited": - if (content) { - content.windowUtils.exitFullscreen(); - } - break; - case "GeckoView:ZoomToInput": { - const dwu = content.windowUtils; - - const zoomToFocusedInput = function() { - if (!dwu.flushApzRepaints()) { - dwu.zoomToFocusedInput(); - return; - } - Services.obs.addObserver(function apzFlushDone() { - Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed"); - dwu.zoomToFocusedInput(); - }, "apz-repaints-flushed"); - }; - - const { force } = aMsg.data; - - let gotResize = false; - const onResize = function() { - gotResize = true; - if (dwu.isMozAfterPaintPending) { - addEventListener( - "MozAfterPaint", - function paintDone() { - removeEventListener("MozAfterPaint", paintDone, { - capture: true, - }); - zoomToFocusedInput(); - }, - { capture: true } - ); - } else { - zoomToFocusedInput(); - } - }; - - addEventListener("resize", onResize, { capture: true }); - - // When the keyboard is displayed, we can get one resize event, - // multiple resize events, or none at all. Try to handle all these - // cases by allowing resizing within a set interval, and still zoom to - // input if there is no resize event at the end of the interval. - content.setTimeout(() => { - removeEventListener("resize", onResize, { capture: true }); - if (!gotResize && force) { - onResize(); - } - }, 500); - break; - } - - case "GeckoView:RestoreState": - const { history, formdata, scrolldata, loadOptions } = aMsg.data; - this._savedState = { history, formdata, scrolldata }; - - if (history) { - const restoredHistory = SessionHistory.restore(docShell, history); - - addEventListener( - "load", - _ => { - if (formdata) { - this.Utils.restoreFrameTreeData( - content, - formdata, - (frame, data) => { - // restore() will return false, and thus abort restoration for the - // current |frame| and its descendants, if |data.url| is given but - // doesn't match the loaded document's URL. - return SessionStoreUtils.restoreFormData( - frame.document, - data - ); - } - ); - } - }, - { capture: true, mozSystemGroup: true, once: true } - ); - - const scrollRestore = _ => { - if (content.location != "about:blank") { - if (scrolldata) { - this.Utils.restoreFrameTreeData( - content, - scrolldata, - (frame, data) => { - if (data.scroll) { - SessionStoreUtils.restoreScrollPosition(frame, data); - } - } - ); - } - delete this._savedState; - removeEventListener("pageshow", scrollRestore, { - capture: true, - mozSystemGroup: true, - }); - } - }; - - addEventListener("pageshow", scrollRestore, { - capture: true, - mozSystemGroup: true, - }); - - if (!this.progressFilter) { - this.progressFilter = Cc[ - "@mozilla.org/appshell/component/browser-status-filter;1" - ].createInstance(Ci.nsIWebProgress); - this.flags = Ci.nsIWebProgress.NOTIFY_LOCATION; - } - - this.progressFilter.addProgressListener(this, this.flags); - const webProgress = docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - webProgress.addProgressListener(this.progressFilter, this.flags); - - this.loadEntry(loadOptions, restoredHistory); - } - break; - - case "GeckoView:UpdateInitData": - // Provide a hook for native code to detect a transfer. - Services.obs.notifyObservers( - docShell, - "geckoview-content-global-transferred" - ); - break; - case "GeckoView:ScrollBy": - const x = {}; - const y = {}; - content.windowUtils.getVisualViewportOffset(x, y); - content.windowUtils.scrollToVisual( - x.value + this.toPixels(aMsg.data.widthValue, aMsg.data.widthType), - y.value + this.toPixels(aMsg.data.heightValue, aMsg.data.heightType), - content.windowUtils.UPDATE_TYPE_MAIN_THREAD, - this.toScrollBehavior(aMsg.data.behavior) - ); - break; - case "GeckoView:ScrollTo": - content.windowUtils.scrollToVisual( - this.toPixels(aMsg.data.widthValue, aMsg.data.widthType), - this.toPixels(aMsg.data.heightValue, aMsg.data.heightType), - content.windowUtils.UPDATE_TYPE_MAIN_THREAD, - this.toScrollBehavior(aMsg.data.behavior) - ); - break; - } - } - - loadEntry(loadOptions, history) { - if (!loadOptions) { - history.QueryInterface(Ci.nsISHistory).reloadCurrentEntry(); - return; - } - - const webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - - const { - referrerInfo, - triggeringPrincipal, - uri, - flags, - csp, - headers, - } = loadOptions; - - webNavigation.loadURI(uri, { - triggeringPrincipal: E10SUtils.deserializePrincipal(triggeringPrincipal), - referrerInfo: E10SUtils.deserializeReferrerInfo(referrerInfo), - loadFlags: flags, - csp: E10SUtils.deserializeCSP(csp), - headers, - }); - } - - // eslint-disable-next-line complexity - handleEvent(aEvent) { - debug`handleEvent: ${aEvent.type}`; - - switch (aEvent.type) { - case "contextmenu": - function nearestParentAttribute(aNode, aAttribute) { - while ( - aNode && - aNode.hasAttribute && - !aNode.hasAttribute(aAttribute) - ) { - aNode = aNode.parentNode; - } - return aNode && aNode.getAttribute && aNode.getAttribute(aAttribute); - } - - function createAbsoluteUri(aBaseUri, aUri) { - if (!aUri || !aBaseUri || !aBaseUri.displaySpec) { - return null; - } - return Services.io.newURI(aUri, null, aBaseUri).displaySpec; - } - - const node = aEvent.composedTarget; - const baseUri = node.ownerDocument.baseURIObject; - const uri = createAbsoluteUri( - baseUri, - nearestParentAttribute(node, "href") - ); - const title = nearestParentAttribute(node, "title"); - const alt = nearestParentAttribute(node, "alt"); - const elementType = ChromeUtils.getClassName(node); - const isImage = elementType === "HTMLImageElement"; - const isMedia = - elementType === "HTMLVideoElement" || - elementType === "HTMLAudioElement"; - const elementSrc = - (isImage || isMedia) && (node.currentSrc || node.src); - - if (uri || isImage || isMedia) { - const msg = { - type: "GeckoView:ContextMenu", - screenX: aEvent.screenX, - screenY: aEvent.screenY, - baseUri: (baseUri && baseUri.displaySpec) || null, - uri, - title, - alt, - elementType, - elementSrc: elementSrc || null, - }; - - this.eventDispatcher.sendRequest(msg); - aEvent.preventDefault(); - } - break; - case "MozDOMFullscreen:Request": - sendAsyncMessage("GeckoView:DOMFullscreenRequest"); - break; - case "MozDOMFullscreen:Entered": - case "MozDOMFullscreen:Exited": - // Content may change fullscreen state by itself, and we should ensure - // that the parent always exits fullscreen when content has left - // full screen mode. - if (content && content.document.fullscreenElement) { - break; - } - // fall-through - case "MozDOMFullscreen:Exit": - sendAsyncMessage("GeckoView:DOMFullscreenExit"); - break; - case "DOMMetaViewportFitChanged": - if (aEvent.originalTarget.ownerGlobal == content) { - this.notifyParentOfViewportFit(); - } - break; - case "DOMTitleChanged": - this.eventDispatcher.sendRequest({ - type: "GeckoView:DOMTitleChanged", - title: content.document.title, - }); - break; - case "DOMWindowClose": - if (!aEvent.isTrusted) { - return; - } - - aEvent.preventDefault(); - this.eventDispatcher.sendRequest({ - type: "GeckoView:DOMWindowClose", - }); - break; - case "mozcaretstatechanged": - if ( - aEvent.reason === "presscaret" || - aEvent.reason === "releasecaret" - ) { - this.eventDispatcher.sendRequest({ - type: "GeckoView:PinOnScreen", - pinned: aEvent.reason === "presscaret", - }); - } - break; - case "DOMContentLoaded": { - if (aEvent.originalTarget.ownerGlobal == content) { - // If loaded content doesn't have viewport-fit, parent still - // uses old value of previous content. - this.notifyParentOfViewportFit(); - } - content.requestIdleCallback(async () => { - const manifest = await ManifestObtainer.contentObtainManifest( - content - ); - if (manifest) { - this.eventDispatcher.sendRequest({ - type: "GeckoView:WebAppManifest", - manifest, - }); - } - }); - break; - } - case "MozFirstContentfulPaint": - this.eventDispatcher.sendRequest({ - type: "GeckoView:FirstContentfulPaint", - }); - break; - } - } - - // WebProgress event handler. - onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { - debug`onLocationChange`; - - if (this._savedState) { - const scrolldata = this._savedState.scrolldata; - if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) { - const utils = content.windowUtils; - // Restore zoom level. - utils.setRestoreResolution( - scrolldata.zoom.resolution, - scrolldata.zoom.displaySize.width, - scrolldata.zoom.displaySize.height - ); - } - } - - this.progressFilter.removeProgressListener(this); - const webProgress = docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - webProgress.removeProgressListener(this.progressFilter); - } -} - -const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent"); // eslint-disable-line no-unused-vars -const module = GeckoViewContentChild.create(this); diff --git a/mobile/android/chrome/geckoview/extension-content.js b/mobile/android/chrome/geckoview/extension-content.js new file mode 100644 index 000000000000..0f49dff6f55b --- /dev/null +++ b/mobile/android/chrome/geckoview/extension-content.js @@ -0,0 +1,12 @@ +/* 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/. */ + +/* This is needed for extensions to load content scripts */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Send this notification from the content process so that the +// ExtensionPolicyService can register this content frame and be ready to load +// content scripts +Services.obs.notifyObservers(this, "tab-content-frameloader-created"); diff --git a/mobile/android/chrome/geckoview/geckoview.js b/mobile/android/chrome/geckoview/geckoview.js index b4b0c2060a78..c119197c1daf 100644 --- a/mobile/android/chrome/geckoview/geckoview.js +++ b/mobile/android/chrome/geckoview/geckoview.js @@ -235,7 +235,7 @@ var ModuleManager = { module.enabled = true; }); - this.messageManager.sendAsyncMessage( + this.getActor("GeckoViewContent").sendAsyncMessage( "GeckoView:RestoreState", sessionState ); @@ -508,11 +508,16 @@ function startup() { const browser = createBrowser(); ModuleManager.init(browser, [ + { + name: "ExtensionContent", + onInit: { + frameScript: "chrome://geckoview/content/extension-content.js", + }, + }, { name: "GeckoViewContent", onInit: { resource: "resource://gre/modules/GeckoViewContent.jsm", - frameScript: "chrome://geckoview/content/GeckoViewContentChild.js", }, }, { diff --git a/mobile/android/chrome/geckoview/jar.mn b/mobile/android/chrome/geckoview/jar.mn index 7d44e3f1a8d3..c8f49a226661 100644 --- a/mobile/android/chrome/geckoview/jar.mn +++ b/mobile/android/chrome/geckoview/jar.mn @@ -9,10 +9,10 @@ geckoview.jar: content/config.js % override chrome://global/content/config.xhtml chrome://geckoview/content/config.xhtml + content/extension-content.js content/geckoview.xhtml content/geckoview.js content/GeckoViewAutofillChild.js - content/GeckoViewContentChild.js content/GeckoViewMediaChild.js content/GeckoViewProgressChild.js content/GeckoViewPromptChild.js diff --git a/mobile/android/components/geckoview/GeckoViewStartup.jsm b/mobile/android/components/geckoview/GeckoViewStartup.jsm index 321d24f81d28..d04145cc68c4 100644 --- a/mobile/android/components/geckoview/GeckoViewStartup.jsm +++ b/mobile/android/components/geckoview/GeckoViewStartup.jsm @@ -29,8 +29,25 @@ const JSWINDOWACTORS = { }, }, GeckoViewContent: { + parent: { + moduleURI: "resource:///actors/GeckoViewContentParent.jsm", + }, child: { moduleURI: "resource:///actors/GeckoViewContentChild.jsm", + events: { + DOMContentLoaded: {}, + DOMMetaViewportFitChanged: {}, + DOMTitleChanged: {}, + DOMWindowClose: {}, + "MozDOMFullscreen:Entered": {}, + "MozDOMFullscreen:Exit": {}, + "MozDOMFullscreen:Exited": {}, + "MozDOMFullscreen:Request": {}, + MozFirstContentfulPaint: {}, + contextmenu: { capture: true }, + mozcaretstatechanged: { capture: true, mozSystemGroup: true }, + }, + allFrames: true, }, }, LoadURIDelegate: { diff --git a/mobile/android/modules/geckoview/GeckoViewContent.jsm b/mobile/android/modules/geckoview/GeckoViewContent.jsm index 90ca303fd3db..fe196ce7cefb 100644 --- a/mobile/android/modules/geckoview/GeckoViewContent.jsm +++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm @@ -25,11 +25,12 @@ class GeckoViewContent extends GeckoViewModule { "GeckoView:DisplayMatches", "GeckoView:FindInPage", "GeckoView:RestoreState", - "GeckoView:SetActive", - "GeckoView:SetFocused", - "GeckoView:ZoomToInput", "GeckoView:ScrollBy", "GeckoView:ScrollTo", + "GeckoView:SetActive", + "GeckoView:SetFocused", + "GeckoView:UpdateInitData", + "GeckoView:ZoomToInput", ]); } @@ -53,12 +54,6 @@ class GeckoViewContent extends GeckoViewModule { /* untrusted */ false ); - this.messageManager.addMessageListener("GeckoView:DOMFullscreenExit", this); - this.messageManager.addMessageListener( - "GeckoView:DOMFullscreenRequest", - this - ); - Services.obs.addObserver(this, "oop-frameloader-crashed"); Services.obs.addObserver(this, "ipc:content-shutdown"); } @@ -80,26 +75,46 @@ class GeckoViewContent extends GeckoViewModule { /* capture */ true ); - this.messageManager.removeMessageListener( - "GeckoView:DOMFullscreenExit", - this - ); - this.messageManager.removeMessageListener( - "GeckoView:DOMFullscreenRequest", - this - ); - Services.obs.removeObserver(this, "oop-frameloader-crashed"); Services.obs.removeObserver(this, "ipc:content-shutdown"); } + get actor() { + return this.getActor("GeckoViewContent"); + } + + // Goes up the browsingContext chain and sends the message every time + // we cross the process boundary so that every process in the chain is + // notified. + sendToAllChildren(aEvent, aData) { + let { browsingContext } = this.actor; + + while (browsingContext) { + if (!browsingContext.currentWindowGlobal) { + break; + } + + const currentPid = browsingContext.currentWindowGlobal.osPid; + const parentPid = browsingContext.parent?.currentWindowGlobal.osPid; + + if (currentPid != parentPid) { + const actor = browsingContext.currentWindowGlobal.getActor( + "GeckoViewContent" + ); + actor.sendAsyncMessage(aEvent, aData); + } + + browsingContext = browsingContext.parent; + } + } + // Bundle event handler. onEvent(aEvent, aData, aCallback) { debug`onEvent: event=${aEvent}, data=${aData}`; switch (aEvent) { case "GeckoViewContent:ExitFullScreen": - this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited"); + this.sendToAllChildren("GeckoView:DOMFullscreenExited"); break; case "GeckoView:ClearMatches": { this._clearMatches(); @@ -114,13 +129,18 @@ class GeckoViewContent extends GeckoViewModule { break; } case "GeckoView:ZoomToInput": - this.messageManager.sendAsyncMessage(aEvent, aData); + this.sendToAllChildren(aEvent, aData); break; case "GeckoView:ScrollBy": - this.messageManager.sendAsyncMessage(aEvent, aData); + // Unclear if that actually works with oop iframes? + this.sendToAllChildren(aEvent, aData); break; case "GeckoView:ScrollTo": - this.messageManager.sendAsyncMessage(aEvent, aData); + // Unclear if that actually works with oop iframes? + this.sendToAllChildren(aEvent, aData); + break; + case "GeckoView:UpdateInitData": + this.sendToAllChildren(aEvent, aData); break; case "GeckoView:SetActive": this.browser.docShellIsActive = !!aData.active; @@ -135,7 +155,8 @@ class GeckoViewContent extends GeckoViewModule { } break; case "GeckoView:RestoreState": - this.messageManager.sendAsyncMessage("GeckoView:RestoreState", aData); + // TODO: this needs parent process history to work properly + this.actor.sendAsyncMessage("GeckoView:RestoreState", aData); break; } } @@ -160,27 +181,11 @@ class GeckoViewContent extends GeckoViewModule { case "MozDOMFullscreen:Entered": if (this.browser == aEvent.target) { // Remote browser; dispatch to content process. - this.messageManager.sendAsyncMessage( - "GeckoView:DOMFullscreenEntered" - ); + this.sendToAllChildren("GeckoView:DOMFullscreenEntered"); } break; case "MozDOMFullscreen:Exited": - this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited"); - break; - } - } - - // Message manager event handler. - receiveMessage(aMsg) { - debug`receiveMessage: ${aMsg.name}`; - - switch (aMsg.name) { - case "GeckoView:DOMFullscreenExit": - this.window.windowUtils.remoteFrameFullscreenReverted(); - break; - case "GeckoView:DOMFullscreenRequest": - this.window.windowUtils.remoteFrameFullscreenChanged(aMsg.target); + this.sendToAllChildren("GeckoView:DOMFullscreenExited"); break; } } diff --git a/mobile/android/modules/geckoview/GeckoViewModule.jsm b/mobile/android/modules/geckoview/GeckoViewModule.jsm index 2bb0c83a5207..7edf0fec73b6 100644 --- a/mobile/android/modules/geckoview/GeckoViewModule.jsm +++ b/mobile/android/modules/geckoview/GeckoViewModule.jsm @@ -39,6 +39,10 @@ class GeckoViewModule { return this.moduleManager.window; } + getActor(aActorName) { + return this.moduleManager.getActor(aActorName); + } + get browser() { return this.moduleManager.browser; } diff --git a/testing/web-platform/meta/html/browsers/the-window-object/close-method.window.js.ini b/testing/web-platform/meta/html/browsers/the-window-object/close-method.window.js.ini index 94c3506b03fd..908c70753c30 100644 --- a/testing/web-platform/meta/html/browsers/the-window-object/close-method.window.js.ini +++ b/testing/web-platform/meta/html/browsers/the-window-object/close-method.window.js.ini @@ -1,14 +1,10 @@ [close-method.window.html] - expected: - if (os == "android") and e10s: TIMEOUT [window.close() affects name targeting immediately] expected: - if (os == "android") and e10s: TIMEOUT FAIL [window.close() queues a task to discard, but window.closed knows immediately] expected: if debug and not webrender and not e10s and (os == "linux") and (version == "Ubuntu 16.04") and (processor == "x86") and (bits == 32): FAIL - if (os == "android") and not e10s: FAIL - if (os == "android") and e10s: TIMEOUT + if (os == "android"): FAIL diff --git a/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini b/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini index b249173b455a..4de18c2eb180 100644 --- a/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini +++ b/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini @@ -1,24 +1,18 @@ [self-et-al.window.html] - expected: - if (os == "android") and e10s: TIMEOUT max-asserts: 3 [popupWindow.window before, after closing, and after discarding] expected: - if (os == "android") and not e10s: FAIL - if (os == "android") and e10s: TIMEOUT + if (os == "android"): FAIL [popupWindow.globalThis before, after closing, and after discarding] expected: - if (os == "android") and not e10s: FAIL - if (os == "android") and e10s: TIMEOUT + if (os == "android"): FAIL [popupWindow.frames before, after closing, and after discarding] expected: - if (os == "android") and not e10s: FAIL - if (os == "android") and e10s: TIMEOUT + if (os == "android"): FAIL [popupWindow.self before, after closing, and after discarding] expected: - if (os == "android") and not e10s: FAIL - if (os == "android") and e10s: TIMEOUT + if (os == "android"): FAIL