diff options
Diffstat (limited to 'mobile/android/actors/GeckoViewContentChild.sys.mjs')
-rw-r--r-- | mobile/android/actors/GeckoViewContentChild.sys.mjs | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/mobile/android/actors/GeckoViewContentChild.sys.mjs b/mobile/android/actors/GeckoViewContentChild.sys.mjs new file mode 100644 index 0000000000..c0a19e5b6b --- /dev/null +++ b/mobile/android/actors/GeckoViewContentChild.sys.mjs @@ -0,0 +1,335 @@ +/* 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/. */ + +import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs"; + +// 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; + +const SCREEN_ORIENTATION_PORTRAIT = 0; +const SCREEN_ORIENTATION_LANDSCAPE = 1; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs", + SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", + Utils: "resource://gre/modules/sessionstore/Utils.sys.mjs", +}); + +export class GeckoViewContentChild extends GeckoViewActorChild { + constructor() { + super(); + this.lastOrientation = SCREEN_ORIENTATION_PORTRAIT; + } + + actorCreated() { + super.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 = lazy.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 = lazy.PrivacyFilter.filterFormData(formdata || {}); + + return { history, formdata, scrolldata }; + } + + orientation() { + const currentOrientationType = this.contentWindow?.screen.orientation.type; + if (!currentOrientationType) { + // Unfortunately, we don't know current screen orientation. + // Return portrait as default. + return SCREEN_ORIENTATION_PORTRAIT; + } + if (currentOrientationType.startsWith("landscape")) { + return SCREEN_ORIENTATION_LANDSCAPE; + } + return SCREEN_ORIENTATION_PORTRAIT; + } + + receiveMessage(message) { + const { name } = message; + debug`receiveMessage: ${name}`; + + switch (name) { + case "GeckoView:DOMFullscreenEntered": + this.lastOrientation = this.orientation(); + if ( + !this.contentWindow?.windowUtils.handleFullscreenRequests() && + !this.contentWindow?.document.fullscreenElement + ) { + // If we don't actually have any pending fullscreen request + // to handle, neither we have been in fullscreen, tell the + // parent to just exit. + const actor = + this.contentWindow?.windowGlobalChild?.getActor("ContentDelegate"); + actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {}); + } + break; + case "GeckoView:DOMFullscreenExited": + // During fullscreen, window size is changed. So don't restore viewport size. + const restoreViewSize = this.orientation() == this.lastOrientation; + this.contentWindow?.windowUtils.exitFullscreen(!restoreViewSize); + 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) { + lazy.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(); + } + case "ContainsFormData": { + return this.containsFormData(); + } + } + + return null; + } + + async containsFormData() { + const { contentWindow } = this; + let formdata = SessionStoreUtils.collectFormData(contentWindow); + formdata = lazy.PrivacyFilter.filterFormData(formdata || {}); + if (formdata) { + return true; + } + return false; + } + + 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) { + lazy.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) { + lazy.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}`; + + 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"); |