diff options
Diffstat (limited to '')
-rw-r--r-- | mobile/android/modules/geckoview/GeckoViewContent.jsm | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewContent.jsm b/mobile/android/modules/geckoview/GeckoViewContent.jsm new file mode 100644 index 0000000000..3235e01ed6 --- /dev/null +++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm @@ -0,0 +1,457 @@ +/* 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 = ["GeckoViewContent"]; + +const { GeckoViewModule } = ChromeUtils.import( + "resource://gre/modules/GeckoViewModule.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +class GeckoViewContent extends GeckoViewModule { + onInit() { + this.registerListener([ + "GeckoViewContent:ExitFullScreen", + "GeckoView:ClearMatches", + "GeckoView:DisplayMatches", + "GeckoView:FindInPage", + "GeckoView:RestoreState", + "GeckoView:ContainsFormData", + "GeckoView:ScrollBy", + "GeckoView:ScrollTo", + "GeckoView:SetActive", + "GeckoView:SetFocused", + "GeckoView:SetPriorityHint", + "GeckoView:UpdateInitData", + "GeckoView:ZoomToInput", + ]); + } + + onEnable() { + this.window.addEventListener( + "MozDOMFullscreen:Entered", + this, + /* capture */ true, + /* untrusted */ false + ); + this.window.addEventListener( + "MozDOMFullscreen:Exited", + this, + /* capture */ true, + /* untrusted */ false + ); + this.window.addEventListener( + "framefocusrequested", + this, + /* capture */ true, + /* untrusted */ false + ); + + this.window.addEventListener("DOMWindowClose", this); + this.window.addEventListener("pagetitlechanged", this); + this.window.addEventListener("pageinfo", this); + + this.window.addEventListener("cookiebannerdetected", this); + this.window.addEventListener("cookiebannerhandled", this); + + Services.obs.addObserver(this, "oop-frameloader-crashed"); + Services.obs.addObserver(this, "ipc:content-shutdown"); + } + + onDisable() { + this.window.removeEventListener( + "MozDOMFullscreen:Entered", + this, + /* capture */ true + ); + this.window.removeEventListener( + "MozDOMFullscreen:Exited", + this, + /* capture */ true + ); + this.window.removeEventListener( + "framefocusrequested", + this, + /* capture */ true + ); + + this.window.removeEventListener("DOMWindowClose", this); + this.window.removeEventListener("pagetitlechanged", this); + this.window.removeEventListener("pageinfo", this); + + this.window.removeEventListener("cookiebannerdetected", this); + this.window.removeEventListener("cookiebannerhandled", 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.browser.ownerDocument.exitFullscreen(); + break; + case "GeckoView:ClearMatches": { + this._clearMatches(); + break; + } + case "GeckoView:DisplayMatches": { + this._displayMatches(aData); + break; + } + case "GeckoView:FindInPage": { + this._findInPage(aData, aCallback); + break; + } + case "GeckoView:ZoomToInput": + this.sendToAllChildren(aEvent, aData); + break; + case "GeckoView:ScrollBy": + // Unclear if that actually works with oop iframes? + this.sendToAllChildren(aEvent, aData); + break; + case "GeckoView:ScrollTo": + // 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; + break; + case "GeckoView:SetFocused": + if (aData.focused) { + this.browser.focus(); + this.browser.setAttribute("primary", "true"); + } else { + this.browser.removeAttribute("primary"); + this.browser.blur(); + } + break; + case "GeckoView:SetPriorityHint": + if (this.browser.isRemoteBrowser) { + const remoteTab = this.browser.frameLoader?.remoteTab; + if (remoteTab) { + remoteTab.priorityHint = aData.priorityHint; + } + } + break; + case "GeckoView:RestoreState": + this.actor.restoreState(aData); + break; + case "GeckoView:ContainsFormData": + this._containsFormData(aCallback); + break; + } + } + + // DOM event handler + handleEvent(aEvent) { + debug`handleEvent: ${aEvent.type}`; + + switch (aEvent.type) { + case "framefocusrequested": + if (this.browser != aEvent.target) { + return; + } + if (this.browser.hasAttribute("primary")) { + return; + } + this.eventDispatcher.sendRequest({ + type: "GeckoView:FocusRequest", + }); + aEvent.preventDefault(); + break; + case "MozDOMFullscreen:Entered": + if (this.browser == aEvent.target) { + // Remote browser; dispatch to content process. + this.sendToAllChildren("GeckoView:DOMFullscreenEntered"); + } + break; + case "MozDOMFullscreen:Exited": + this.sendToAllChildren("GeckoView:DOMFullscreenExited"); + break; + case "pagetitlechanged": + this.eventDispatcher.sendRequest({ + type: "GeckoView:PageTitleChanged", + title: this.browser.contentTitle, + }); + break; + case "DOMWindowClose": + // We need this because we want to allow the app + // to close the window itself. If we don't preventDefault() + // here Gecko will close it immediately. + aEvent.preventDefault(); + + this.eventDispatcher.sendRequest({ + type: "GeckoView:DOMWindowClose", + }); + break; + case "pageinfo": + if (aEvent.detail.previewImageURL) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:PreviewImage", + previewImageUrl: aEvent.detail.previewImageURL, + }); + } + break; + case "cookiebannerdetected": + this.eventDispatcher.sendRequest({ + type: "GeckoView:CookieBannerEvent:Detected", + }); + break; + case "cookiebannerhandled": + this.eventDispatcher.sendRequest({ + type: "GeckoView:CookieBannerEvent:Handled", + }); + break; + } + } + + // nsIObserver event handler + observe(aSubject, aTopic, aData) { + debug`observe: ${aTopic}`; + this._contentCrashed = false; + const browser = aSubject.ownerElement; + + switch (aTopic) { + case "oop-frameloader-crashed": { + if (!browser || browser != this.browser) { + return; + } + this.window.setTimeout(() => { + if (this._contentCrashed) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:ContentCrash", + }); + } else { + this.eventDispatcher.sendRequest({ + type: "GeckoView:ContentKill", + }); + } + }, 250); + break; + } + case "ipc:content-shutdown": { + aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (aSubject.get("dumpID")) { + if ( + browser && + aSubject.get("childID") != browser.frameLoader.childID + ) { + return; + } + this._contentCrashed = true; + } + break; + } + } + } + + async _containsFormData(aCallback) { + aCallback.onSuccess(await this.actor.containsFormData()); + } + + _findInPage(aData, aCallback) { + debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`; + + let finder; + try { + finder = this.browser.finder; + } catch (e) { + if (aCallback) { + aCallback.onError(`No finder: ${e}`); + } + return; + } + + if (this._finderListener) { + finder.removeResultListener(this._finderListener); + } + + this._finderListener = { + response: { + found: false, + wrapped: false, + current: 0, + total: -1, + searchString: aData.searchString || finder.searchString, + linkURL: null, + clientRect: null, + flags: { + backwards: !!aData.backwards, + linksOnly: !!aData.linksOnly, + matchCase: !!aData.matchCase, + wholeWord: !!aData.wholeWord, + }, + }, + + onFindResult(aOptions) { + if (!aCallback || aOptions.searchString !== aData.searchString) { + // Result from a previous search. + return; + } + + Object.assign(this.response, { + found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND, + wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND, + linkURL: aOptions.linkURL, + clientRect: aOptions.rect && { + left: aOptions.rect.left, + top: aOptions.rect.top, + right: aOptions.rect.right, + bottom: aOptions.rect.bottom, + }, + flags: { + backwards: aOptions.findBackwards, + linksOnly: aOptions.linksOnly, + matchCase: this.response.flags.matchCase, + wholeWord: this.response.flags.wholeWord, + }, + }); + + if (!this.response.found) { + this.response.current = 0; + this.response.total = 0; + } + + // Only send response if we have a count. + if (!this.response.found || this.response.current !== 0) { + debug`onFindResult: ${this.response}`; + aCallback.onSuccess(this.response); + aCallback = undefined; + } + }, + + onMatchesCountResult(aResult) { + if (!aCallback || finder.searchString !== aData.searchString) { + // Result from a previous search. + return; + } + + Object.assign(this.response, { + current: aResult.current, + total: aResult.total, + }); + + // Only send response if we have a result. `found` and `wrapped` are + // both false only when we haven't received a result yet. + if (this.response.found || this.response.wrapped) { + debug`onMatchesCountResult: ${this.response}`; + aCallback.onSuccess(this.response); + aCallback = undefined; + } + }, + + onCurrentSelection() {}, + + onHighlightFinished() {}, + }; + + finder.caseSensitive = !!aData.matchCase; + finder.entireWord = !!aData.wholeWord; + finder.matchDiacritics = !!aData.matchDiacritics; + finder.addResultListener(this._finderListener); + + const drawOutline = + this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline; + + if (!aData.searchString || aData.searchString === finder.searchString) { + // Search again. + aData.searchString = finder.searchString; + finder.findAgain( + aData.searchString, + !!aData.backwards, + !!aData.linksOnly, + drawOutline + ); + } else { + finder.fastFind(aData.searchString, !!aData.linksOnly, drawOutline); + } + } + + _clearMatches() { + debug`clearMatches`; + + let finder; + try { + finder = this.browser.finder; + } catch (e) { + return; + } + + finder.removeSelection(); + finder.highlight(false); + + if (this._finderListener) { + finder.removeResultListener(this._finderListener); + this._finderListener = null; + } + } + + _displayMatches(aData) { + debug`displayMatches: data=${aData}`; + + let finder; + try { + finder = this.browser.finder; + } catch (e) { + return; + } + + this._matchDisplayOptions = aData; + finder.onModalHighlightChange(!!aData.dimPage); + finder.onHighlightAllChange(!!aData.highlightAll); + + if (!aData.highlightAll && !aData.dimPage) { + finder.highlight(false); + return; + } + + if (!this._finderListener || !finder.searchString) { + return; + } + const linksOnly = this._finderListener.response.linksOnly; + finder.highlight(true, finder.searchString, linksOnly, !!aData.drawOutline); + } +} + +const { debug, warn } = GeckoViewContent.initLogging("GeckoViewContent"); |