diff options
Diffstat (limited to 'mobile/android/actors')
-rw-r--r-- | mobile/android/actors/ContentDelegateChild.jsm | 166 | ||||
-rw-r--r-- | mobile/android/actors/ContentDelegateParent.jsm | 34 | ||||
-rw-r--r-- | mobile/android/actors/GeckoViewContentChild.jsm | 301 | ||||
-rw-r--r-- | mobile/android/actors/GeckoViewContentParent.jsm | 67 | ||||
-rw-r--r-- | mobile/android/actors/GeckoViewPromptChild.jsm | 38 | ||||
-rw-r--r-- | mobile/android/actors/GeckoViewSettingsChild.jsm | 58 | ||||
-rw-r--r-- | mobile/android/actors/LoadURIDelegateChild.jsm | 95 | ||||
-rw-r--r-- | mobile/android/actors/ProgressDelegateChild.jsm | 41 | ||||
-rw-r--r-- | mobile/android/actors/ProgressDelegateParent.jsm | 12 | ||||
-rw-r--r-- | mobile/android/actors/ScrollDelegateChild.jsm | 46 | ||||
-rw-r--r-- | mobile/android/actors/SelectionActionDelegateChild.jsm | 310 | ||||
-rw-r--r-- | mobile/android/actors/WebBrowserChromeChild.jsm | 55 | ||||
-rw-r--r-- | mobile/android/actors/moz.build | 21 |
13 files changed, 1244 insertions, 0 deletions
diff --git a/mobile/android/actors/ContentDelegateChild.jsm b/mobile/android/actors/ContentDelegateChild.jsm new file mode 100644 index 0000000000..9cb90793c1 --- /dev/null +++ b/mobile/android/actors/ContentDelegateChild.jsm @@ -0,0 +1,166 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + ManifestObtainer: "resource://gre/modules/ManifestObtainer.jsm", +}); + +var EXPORTED_SYMBOLS = ["ContentDelegateChild"]; + +class ContentDelegateChild extends GeckoViewActorChild { + 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, + }); + } + ); + } + + // eslint-disable-next-line complexity + handleEvent(aEvent) { + debug`handleEvent: ${aEvent.type}`; + if (!this.isContentWindow) { + // This not a GeckoView-controlled window + return; + } + + 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 "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(); + } + if (this.contentWindow !== this.contentWindow?.top) { + // Only check WebApp manifest on the top level window. + return; + } + 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; + } + case "MozPaintStatusReset": { + this.eventDispatcher.sendRequest({ + type: "GeckoView:PaintStatusReset", + }); + break; + } + } + } +} + +const { debug, warn } = ContentDelegateChild.initLogging( + "ContentDelegateChild" +); diff --git a/mobile/android/actors/ContentDelegateParent.jsm b/mobile/android/actors/ContentDelegateParent.jsm new file mode 100644 index 0000000000..7bc4de958c --- /dev/null +++ b/mobile/android/actors/ContentDelegateParent.jsm @@ -0,0 +1,34 @@ +/* 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 = ["ContentDelegateParent"]; + +const { GeckoViewUtils } = ChromeUtils.import( + "resource://gre/modules/GeckoViewUtils.jsm" +); + +const { GeckoViewActorParent } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorParent.jsm" +); + +const { debug, warn } = GeckoViewUtils.initLogging("ContentDelegateParent"); + +class ContentDelegateParent extends GeckoViewActorParent { + async receiveMessage(aMsg) { + debug`receiveMessage: ${aMsg.name} ${aMsg}`; + + switch (aMsg.name) { + case "GeckoView:DOMFullscreenExit": { + this.window.windowUtils.remoteFrameFullscreenReverted(); + break; + } + + case "GeckoView:DOMFullscreenRequest": { + this.window.windowUtils.remoteFrameFullscreenChanged(this.browser); + break; + } + } + } +} diff --git a/mobile/android/actors/GeckoViewContentChild.jsm b/mobile/android/actors/GeckoViewContentChild.jsm new file mode 100644 index 0000000000..e05e02ecb4 --- /dev/null +++ b/mobile/android/actors/GeckoViewContentChild.jsm @@ -0,0 +1,301 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.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", + 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 { + actorCreated() { + this.pageShow = new Promise(resolve => { + this.receivedPageShow = resolve; + }); + } + + 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; + } + + collectSessionState() { + const { docShell, contentWindow } = this; + const history = SessionHistory.collect(docShell); + let formdata = SessionStoreUtils.collectFormData(contentWindow); + let scrolldata = SessionStoreUtils.collectScrollPosition(contentWindow); + + // Save the current document resolution. + let zoom = 1; + const domWindowUtils = contentWindow.windowUtils; + zoom = domWindowUtils.getResolution(); + scrolldata = scrolldata || {}; + scrolldata.zoom = {}; + scrolldata.zoom.resolution = zoom; + + // Save some data that'll help in adjusting the zoom level + // when restoring in a different screen orientation. + const displaySize = {}; + const width = {}, + height = {}; + domWindowUtils.getContentViewerSize(width, height); + + displaySize.width = width.value; + displaySize.height = height.value; + + scrolldata.zoom.displaySize = displaySize; + + formdata = PrivacyFilter.filterFormData(formdata || {}); + + return { history, formdata, scrolldata }; + } + + 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.windowRoot.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 "RestoreSessionState": { + this.restoreSessionState(message); + break; + } + case "RestoreHistoryAndNavigate": { + const { history, switchId } = message.data; + if (history) { + SessionHistory.restore(this.docShell, history); + const historyIndex = history.requestedIndex - 1; + const webNavigation = this.docShell.QueryInterface( + Ci.nsIWebNavigation + ); + + if (!switchId) { + // TODO: Bug 1648158 This won't work for Fission or HistoryInParent. + webNavigation.sessionHistory.legacySHistory.reloadCurrentEntry(); + } else { + webNavigation.resumeRedirectedLoad(switchId, historyIndex); + } + } + 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(); + } + } + + return null; + } + + async restoreSessionState(message) { + // Make sure we showed something before restoring scrolling and form data + await this.pageShow; + + const { contentWindow } = this; + const { formdata, scrolldata } = message.data; + + 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); + }); + } + + if (scrolldata) { + Utils.restoreFrameTreeData(contentWindow, scrolldata, (frame, data) => { + if (data.scroll) { + SessionStoreUtils.restoreScrollPosition(frame, data); + } + }); + } + + 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 + ); + } + } + + // eslint-disable-next-line complexity + handleEvent(aEvent) { + debug`handleEvent: ${aEvent.type}`; + if (!this.isContentWindow) { + // This is not a GeckoView-controlled window + return; + } + + switch (aEvent.type) { + case "pageshow": { + this.receivedPageShow(); + break; + } + + case "mozcaretstatechanged": + if ( + aEvent.reason === "presscaret" || + aEvent.reason === "releasecaret" + ) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:PinOnScreen", + pinned: aEvent.reason === "presscaret", + }); + } + break; + } + } +} + +const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent"); diff --git a/mobile/android/actors/GeckoViewContentParent.jsm b/mobile/android/actors/GeckoViewContentParent.jsm new file mode 100644 index 0000000000..c25206bca8 --- /dev/null +++ b/mobile/android/actors/GeckoViewContentParent.jsm @@ -0,0 +1,67 @@ +/* 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 { GeckoViewActorParent } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorParent.jsm" +); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewContentParent"); + +class GeckoViewContentParent extends GeckoViewActorParent { + async collectState() { + return this.sendQuery("CollectSessionState"); + } + + restoreState({ history, switchId, formdata, scrolldata }) { + // Restoring is made of two parts. First we need to restore the history + // of the tab and navigating to the current page, after the page + // navigates to the current page we need to restore the state of the + // page (scroll position, form data, etc). + // + // We can't do everything in one step inside the child actor because + // the actor is recreated when navigating, so we need to keep the state + // on the parent side until we navigate. + this.sendAsyncMessage("RestoreHistoryAndNavigate", { + history, + switchId, + }); + + if (!formdata && !scrolldata) { + return; + } + + const progressFilter = Cc[ + "@mozilla.org/appshell/component/browser-status-filter;1" + ].createInstance(Ci.nsIWebProgress); + + const { browser } = this; + const progressListener = { + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener"]), + + onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + if (!aWebProgress.isTopLevel) { + return; + } + // The actor might get recreated between navigations so we need to + // query it again for the up-to-date instance. + browser.browsingContext.currentWindowGlobal + .getActor("GeckoViewContent") + .sendAsyncMessage("RestoreSessionState", { formdata, scrolldata }); + progressFilter.removeProgressListener(this); + browser.removeProgressListener(progressFilter); + }, + }; + + const flags = Ci.nsIWebProgress.NOTIFY_LOCATION; + progressFilter.addProgressListener(progressListener, flags); + browser.addProgressListener(progressFilter, flags); + } +} diff --git a/mobile/android/actors/GeckoViewPromptChild.jsm b/mobile/android/actors/GeckoViewPromptChild.jsm new file mode 100644 index 0000000000..019a300c7d --- /dev/null +++ b/mobile/android/actors/GeckoViewPromptChild.jsm @@ -0,0 +1,38 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gPrompter", + "@mozilla.org/prompter;1", + "nsIPromptService" +); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const EXPORTED_SYMBOLS = ["GeckoViewPromptChild"]; + +class GeckoViewPromptChild extends GeckoViewActorChild { + handleEvent(event) { + const { type } = event; + debug`handleEvent: ${type}`; + + switch (type) { + case "click": // fall-through + case "contextmenu": // fall-through + case "DOMPopupBlocked": + gPrompter.wrappedJSObject.handleEvent(event); + } + } +} + +const { debug, warn } = GeckoViewPromptChild.initLogging("GeckoViewPrompt"); diff --git a/mobile/android/actors/GeckoViewSettingsChild.jsm b/mobile/android/actors/GeckoViewSettingsChild.jsm new file mode 100644 index 0000000000..eed2a413f3 --- /dev/null +++ b/mobile/android/actors/GeckoViewSettingsChild.jsm @@ -0,0 +1,58 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const EXPORTED_SYMBOLS = ["GeckoViewSettingsChild"]; + +// This needs to match GeckoSessionSettings.java +const VIEWPORT_MODE_MOBILE = 0; +const VIEWPORT_MODE_DESKTOP = 1; + +// Handles GeckoView content settings +class GeckoViewSettingsChild extends GeckoViewActorChild { + receiveMessage(message) { + const { name } = message; + debug`receiveMessage: ${name}`; + + switch (name) { + case "SettingsUpdate": { + const settings = message.data; + + this.allowJavascript = settings.allowJavascript; + this.viewportMode = settings.viewportMode; + if (settings.isPopup) { + // Allow web extensions to close their own action popups (bz1612363) + this.contentWindow.windowUtils.allowScriptsToClose(); + } + } + } + } + + get allowJavascript() { + return this.docShell.allowJavascript; + } + + set allowJavascript(aAllowJavascript) { + this.docShell.allowJavascript = aAllowJavascript; + } + + set viewportMode(aMode) { + const { windowUtils } = this.contentWindow; + if (aMode === windowUtils.desktopModeViewport) { + return; + } + windowUtils.desktopModeViewport = aMode === VIEWPORT_MODE_DESKTOP; + } +} + +const { debug, warn } = GeckoViewSettingsChild.initLogging("GeckoViewSettings"); diff --git a/mobile/android/actors/LoadURIDelegateChild.jsm b/mobile/android/actors/LoadURIDelegateChild.jsm new file mode 100644 index 0000000000..b4a84b4474 --- /dev/null +++ b/mobile/android/actors/LoadURIDelegateChild.jsm @@ -0,0 +1,95 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); +const { LoadURIDelegate } = ChromeUtils.import( + "resource://gre/modules/LoadURIDelegate.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + E10SUtils: "resource://gre/modules/E10SUtils.jsm", +}); + +var EXPORTED_SYMBOLS = ["LoadURIDelegateChild"]; + +// Implements nsILoadURIDelegate. +class LoadURIDelegateChild extends GeckoViewActorChild { + // nsILoadURIDelegate. + loadURI(aUri, aWhere, aFlags, aTriggeringPrincipal) { + debug`loadURI: uri=${aUri && aUri.spec} + where=${aWhere} flags=0x${aFlags.toString(16)} + tp=${aTriggeringPrincipal && aTriggeringPrincipal.spec}`; + if (!this.isContentWindow) { + debug`loadURI: not a content window`; + // This is an internal Gecko window, nothing to do + return false; + } + + // Ignore any load going to the extension process + // TODO: Remove workaround after Bug 1619798 + if ( + WebExtensionPolicy.useRemoteWebExtensions && + E10SUtils.getRemoteTypeForURIObject( + aUri, + /* aMultiProcess */ true, + /* aRemoteSubframes */ false, + Services.appinfo.remoteType + ) == E10SUtils.EXTENSION_REMOTE_TYPE + ) { + debug`Bypassing load delegate in the Extension process.`; + return false; + } + + return LoadURIDelegate.load( + this.contentWindow, + this.eventDispatcher, + aUri, + aWhere, + aFlags, + aTriggeringPrincipal + ); + } + + // nsILoadURIDelegate. + handleLoadError(aUri, aError, aErrorModule) { + debug`handleLoadError: uri=${aUri && aUri.spec} + displaySpec=${aUri && aUri.displaySpec} + error=${aError}`; + if (!this.isContentWindow) { + // This is an internal Gecko window, nothing to do + debug`handleLoadError: not a content window`; + return null; + } + + if (aUri && LoadURIDelegate.isSafeBrowsingError(aError)) { + const message = { + type: "GeckoView:ContentBlocked", + uri: aUri.spec, + error: aError, + }; + + this.eventDispatcher.sendRequest(message); + } + + return LoadURIDelegate.handleLoadError( + this.contentWindow, + this.eventDispatcher, + aUri, + aError, + aErrorModule + ); + } +} + +LoadURIDelegateChild.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsILoadURIDelegate", +]); + +const { debug, warn } = LoadURIDelegateChild.initLogging("LoadURIDelegate"); diff --git a/mobile/android/actors/ProgressDelegateChild.jsm b/mobile/android/actors/ProgressDelegateChild.jsm new file mode 100644 index 0000000000..9ab9bbf5b4 --- /dev/null +++ b/mobile/android/actors/ProgressDelegateChild.jsm @@ -0,0 +1,41 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const EXPORTED_SYMBOLS = ["ProgressDelegateChild"]; + +class ProgressDelegateChild extends GeckoViewActorChild { + // eslint-disable-next-line complexity + handleEvent(aEvent) { + debug`handleEvent: ${aEvent.type}`; + if (!this.isContentWindow) { + // This not a GeckoView-controlled window + return; + } + + switch (aEvent.type) { + case "DOMContentLoaded": // fall-through + case "MozAfterPaint": // fall-through + case "pageshow": { + // Forward to main process + const target = aEvent.originalTarget; + const uri = target?.location.href; + this.sendAsyncMessage(aEvent.type, { + uri, + }); + } + } + } +} + +const { debug, warn } = ProgressDelegateChild.initLogging("ProgressDelegate"); diff --git a/mobile/android/actors/ProgressDelegateParent.jsm b/mobile/android/actors/ProgressDelegateParent.jsm new file mode 100644 index 0000000000..b45ecd37d8 --- /dev/null +++ b/mobile/android/actors/ProgressDelegateParent.jsm @@ -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/. */ +"use strict"; + +const EXPORTED_SYMBOLS = ["ProgressDelegateParent"]; + +const { GeckoViewActorParent } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorParent.jsm" +); + +class ProgressDelegateParent extends GeckoViewActorParent {} diff --git a/mobile/android/actors/ScrollDelegateChild.jsm b/mobile/android/actors/ScrollDelegateChild.jsm new file mode 100644 index 0000000000..d7f185b30d --- /dev/null +++ b/mobile/android/actors/ScrollDelegateChild.jsm @@ -0,0 +1,46 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const EXPORTED_SYMBOLS = ["ScrollDelegateChild"]; + +class ScrollDelegateChild extends GeckoViewActorChild { + // eslint-disable-next-line complexity + handleEvent(aEvent) { + if (!this.isContentWindow) { + // This not a GeckoView-controlled window + return; + } + + if (aEvent.originalTarget.ownerGlobal != this.contentWindow) { + return; + } + + debug`handleEvent: ${aEvent.type}`; + + switch (aEvent.type) { + case "mozvisualscroll": + const x = {}; + const y = {}; + this.contentWindow.windowUtils.getVisualViewportOffset(x, y); + this.eventDispatcher.sendRequest({ + type: "GeckoView:ScrollChanged", + scrollX: x.value, + scrollY: y.value, + }); + break; + } + } +} + +const { debug, warn } = ScrollDelegateChild.initLogging("ScrollDelegate"); diff --git a/mobile/android/actors/SelectionActionDelegateChild.jsm b/mobile/android/actors/SelectionActionDelegateChild.jsm new file mode 100644 index 0000000000..82a7b23a3e --- /dev/null +++ b/mobile/android/actors/SelectionActionDelegateChild.jsm @@ -0,0 +1,310 @@ +/* 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 { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const EXPORTED_SYMBOLS = ["SelectionActionDelegateChild"]; + +// Dispatches GeckoView:ShowSelectionAction and GeckoView:HideSelectionAction to +// the GeckoSession on accessible caret changes. +class SelectionActionDelegateChild extends GeckoViewActorChild { + constructor(aModuleName, aMessageManager) { + super(aModuleName, aMessageManager); + + this._seqNo = 0; + this._isActive = false; + this._previousMessage = ""; + } + + _actions = [ + { + id: "org.mozilla.geckoview.HIDE", + predicate: _ => true, + perform: _ => this.handleEvent({ type: "pagehide" }), + }, + { + id: "org.mozilla.geckoview.CUT", + predicate: e => + !e.collapsed && e.selectionEditable && !this._isPasswordField(e), + perform: _ => this.docShell.doCommand("cmd_cut"), + }, + { + id: "org.mozilla.geckoview.COPY", + predicate: e => !e.collapsed && !this._isPasswordField(e), + perform: _ => this.docShell.doCommand("cmd_copy"), + }, + { + id: "org.mozilla.geckoview.PASTE", + predicate: e => + e.selectionEditable && + Services.clipboard.hasDataMatchingFlavors( + ["text/unicode"], + Ci.nsIClipboard.kGlobalClipboard + ), + perform: _ => this._performPaste(), + }, + { + id: "org.mozilla.geckoview.DELETE", + predicate: e => !e.collapsed && e.selectionEditable, + perform: _ => this.docShell.doCommand("cmd_delete"), + }, + { + id: "org.mozilla.geckoview.COLLAPSE_TO_START", + predicate: e => !e.collapsed && e.selectionEditable, + perform: e => this.docShell.doCommand("cmd_moveLeft"), + }, + { + id: "org.mozilla.geckoview.COLLAPSE_TO_END", + predicate: e => !e.collapsed && e.selectionEditable, + perform: e => this.docShell.doCommand("cmd_moveRight"), + }, + { + id: "org.mozilla.geckoview.UNSELECT", + predicate: e => !e.collapsed && !e.selectionEditable, + perform: e => this.docShell.doCommand("cmd_selectNone"), + }, + { + id: "org.mozilla.geckoview.SELECT_ALL", + predicate: e => { + if (e.reason === "longpressonemptycontent") { + return false; + } + if (e.selectionEditable && e.target && e.target.activeElement) { + const element = e.target.activeElement; + let value = ""; + if (element.value) { + value = element.value; + } else if ( + element.isContentEditable || + e.target.designMode === "on" + ) { + value = element.innerText; + } + // Do not show SELECT_ALL if the editable is empty + // or all the editable text is already selected. + return value !== "" && value !== e.selectedTextContent; + } + return true; + }, + perform: e => this.docShell.doCommand("cmd_selectAll"), + }, + ]; + + _performPaste() { + this.handleEvent({ type: "pagehide" }); + this.docShell.doCommand("cmd_paste"); + } + + _isPasswordField(aEvent) { + if (!aEvent.selectionEditable) { + return false; + } + + const win = aEvent.target.defaultView; + const focus = aEvent.target.activeElement; + return ( + win && + win.HTMLInputElement && + focus instanceof win.HTMLInputElement && + !focus.mozIsTextField(/* excludePassword */ true) + ); + } + + _getFrameOffset(aEvent) { + // Get correct offset in case of nested iframe. + const offset = { + left: 0, + top: 0, + }; + + let currentWindow = aEvent.target.defaultView; + while (currentWindow.realFrameElement) { + const frameElement = currentWindow.realFrameElement; + currentWindow = frameElement.ownerGlobal; + + // The offset of the iframe window relative to the parent window + // includes the iframe's border, and the iframe's origin in its + // containing document. + const currentRect = frameElement.getBoundingClientRect(); + const style = currentWindow.getComputedStyle(frameElement); + const borderLeft = parseFloat(style.borderLeftWidth) || 0; + const borderTop = parseFloat(style.borderTopWidth) || 0; + const paddingLeft = parseFloat(style.paddingLeft) || 0; + const paddingTop = parseFloat(style.paddingTop) || 0; + + offset.left += currentRect.left + borderLeft + paddingLeft; + offset.top += currentRect.top + borderTop + paddingTop; + + const targetDocShell = currentWindow.docShell; + if (targetDocShell.isMozBrowser) { + break; + } + } + + // Now we have coordinates relative to the root content document's + // layout viewport. Subtract the offset of the visual viewport + // relative to the layout viewport, to get coordinates relative to + // the visual viewport. + var offsetX = {}; + var offsetY = {}; + currentWindow.windowUtils.getVisualViewportOffsetRelativeToLayoutViewport( + offsetX, + offsetY + ); + offset.left -= offsetX.value; + offset.top -= offsetY.value; + + return offset; + } + + /** + * Receive and act on AccessibleCarets caret state-change + * (mozcaretstatechanged and pagehide) events. + */ + handleEvent(aEvent) { + if (aEvent.type === "pagehide" || aEvent.type === "deactivate") { + // Hide any selection actions on page hide or deactivate. + aEvent = { + reason: "visibilitychange", + caretVisibile: false, + selectionVisible: false, + collapsed: true, + selectionEditable: false, + }; + } + + let reason = aEvent.reason; + + if (this._isActive && !aEvent.caretVisible) { + // For mozcaretstatechanged, "visibilitychange" means the caret is hidden. + reason = "visibilitychange"; + } else if (!aEvent.collapsed && !aEvent.selectionVisible) { + reason = "invisibleselection"; + } else if ( + !this._isActive && + aEvent.selectionEditable && + aEvent.collapsed && + reason !== "longpressonemptycontent" && + reason !== "taponcaret" && + !Services.prefs.getBoolPref( + "geckoview.selection_action.show_on_focus", + false + ) + ) { + // Don't show selection actions when merely focusing on an editor or + // repositioning the cursor. Wait until long press or the caret is tapped + // in order to match Android behavior. + reason = "visibilitychange"; + } + + debug`handleEvent: ${reason}`; + + if ( + [ + "longpressonemptycontent", + "releasecaret", + "taponcaret", + "updateposition", + ].includes(reason) + ) { + const actions = this._actions.filter(action => + action.predicate.call(this, aEvent) + ); + + const offset = this._getFrameOffset(aEvent); + const password = this._isPasswordField(aEvent); + + const msg = { + type: "GeckoView:ShowSelectionAction", + seqNo: this._seqNo, + collapsed: aEvent.collapsed, + editable: aEvent.selectionEditable, + password, + selection: password ? "" : aEvent.selectedTextContent, + clientRect: !aEvent.boundingClientRect + ? null + : { + left: aEvent.boundingClientRect.left + offset.left, + top: aEvent.boundingClientRect.top + offset.top, + right: aEvent.boundingClientRect.right + offset.left, + bottom: aEvent.boundingClientRect.bottom + offset.top, + }, + actions: actions.map(action => action.id), + }; + + try { + if (msg.clientRect) { + msg.clientRect.bottom += parseFloat( + Services.prefs.getCharPref("layout.accessiblecaret.height", "0") + ); + } + } catch (e) {} + + if (this._isActive && JSON.stringify(msg) === this._previousMessage) { + // Don't call again if we're already active and things haven't changed. + return; + } + + msg.seqNo = ++this._seqNo; + this._isActive = true; + this._previousMessage = JSON.stringify(msg); + + this.eventDispatcher.sendRequest(msg, { + onSuccess: response => { + if (response.seqNo !== this._seqNo) { + // Stale action. + warn`Stale response ${response.id}`; + return; + } + const action = actions.find(action => action.id === response.id); + if (action) { + debug`Performing ${response.id}`; + action.perform.call(this, aEvent, response); + } else { + warn`Invalid action ${response.id}`; + } + }, + onError: _ => { + // Do nothing; we can get here if the delegate was just unregistered. + }, + }); + } else if ( + [ + "invisibleselection", + "presscaret", + "scroll", + "visibilitychange", + ].includes(reason) + ) { + if (!this._isActive) { + return; + } + + this._isActive = false; + + // Mark previous actions as stale. Don't do this for "invisibleselection" + // or "scroll" because previous actions should still be valid even after + // these events occur. + if (reason !== "invisibleselection" && reason !== "scroll") { + this._seqNo++; + } + + this.eventDispatcher.sendRequest({ + type: "GeckoView:HideSelectionAction", + reason, + }); + } else { + warn`Unknown reason: ${reason}`; + } + } +} + +const { debug, warn } = SelectionActionDelegateChild.initLogging( + "SelectionActionDelegate" +); diff --git a/mobile/android/actors/WebBrowserChromeChild.jsm b/mobile/android/actors/WebBrowserChromeChild.jsm new file mode 100644 index 0000000000..f3f1dce7ec --- /dev/null +++ b/mobile/android/actors/WebBrowserChromeChild.jsm @@ -0,0 +1,55 @@ +/* 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"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { GeckoViewActorChild } = ChromeUtils.import( + "resource://gre/modules/GeckoViewActorChild.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserUtils: "resource://gre/modules/BrowserUtils.jsm", + GeckoViewSettings: "resource://gre/modules/GeckoViewSettings.jsm", +}); + +var EXPORTED_SYMBOLS = ["WebBrowserChromeChild"]; + +// Implements nsIWebBrowserChrome +class WebBrowserChromeChild extends GeckoViewActorChild { + // nsIWebBrowserChrome + onBeforeLinkTraversal(aOriginalTarget, aLinkURI, aLinkNode, aIsAppTab) { + debug`onBeforeLinkTraversal ${aLinkURI.displaySpec}`; + return BrowserUtils.onBeforeLinkTraversal( + aOriginalTarget, + aLinkURI, + aLinkNode, + aIsAppTab + ); + } + + // nsIWebBrowserChrome + shouldLoadURI( + aDocShell, + aURI, + aReferrerInfo, + aHasPostData, + aTriggeringPrincipal, + aCsp + ) { + return true; + } + + // nsIWebBrowserChrome + shouldLoadURIInThisProcess(aURI) { + return true; + } +} + +WebBrowserChromeChild.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIWebBrowserChrome3", +]); + +const { debug, warn } = WebBrowserChromeChild.initLogging("WebBrowserChrome"); diff --git a/mobile/android/actors/moz.build b/mobile/android/actors/moz.build new file mode 100644 index 0000000000..6e51c42fb1 --- /dev/null +++ b/mobile/android/actors/moz.build @@ -0,0 +1,21 @@ +# 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/. + +with Files("**"): + BUG_COMPONENT = ("GeckoView", "General") + +FINAL_TARGET_FILES.actors += [ + "ContentDelegateChild.jsm", + "ContentDelegateParent.jsm", + "GeckoViewContentChild.jsm", + "GeckoViewContentParent.jsm", + "GeckoViewPromptChild.jsm", + "GeckoViewSettingsChild.jsm", + "LoadURIDelegateChild.jsm", + "ProgressDelegateChild.jsm", + "ProgressDelegateParent.jsm", + "ScrollDelegateChild.jsm", + "SelectionActionDelegateChild.jsm", + "WebBrowserChromeChild.jsm", +] |