summaryrefslogtreecommitdiffstats
path: root/mobile/android/actors
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/actors')
-rw-r--r--mobile/android/actors/ContentDelegateChild.jsm166
-rw-r--r--mobile/android/actors/ContentDelegateParent.jsm34
-rw-r--r--mobile/android/actors/GeckoViewContentChild.jsm301
-rw-r--r--mobile/android/actors/GeckoViewContentParent.jsm67
-rw-r--r--mobile/android/actors/GeckoViewPromptChild.jsm38
-rw-r--r--mobile/android/actors/GeckoViewSettingsChild.jsm58
-rw-r--r--mobile/android/actors/LoadURIDelegateChild.jsm95
-rw-r--r--mobile/android/actors/ProgressDelegateChild.jsm41
-rw-r--r--mobile/android/actors/ProgressDelegateParent.jsm12
-rw-r--r--mobile/android/actors/ScrollDelegateChild.jsm46
-rw-r--r--mobile/android/actors/SelectionActionDelegateChild.jsm310
-rw-r--r--mobile/android/actors/WebBrowserChromeChild.jsm55
-rw-r--r--mobile/android/actors/moz.build21
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",
+]