summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/GeckoViewContent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules/geckoview/GeckoViewContent.sys.mjs')
-rw-r--r--mobile/android/modules/geckoview/GeckoViewContent.sys.mjs762
1 files changed, 762 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewContent.sys.mjs b/mobile/android/modules/geckoview/GeckoViewContent.sys.mjs
new file mode 100644
index 0000000000..b593a6f8e4
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewContent.sys.mjs
@@ -0,0 +1,762 @@
+/* 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 { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
+ ShoppingProduct: "chrome://global/content/shopping/ShoppingProduct.mjs",
+});
+
+export class GeckoViewContent extends GeckoViewModule {
+ onInit() {
+ this.registerListener([
+ "GeckoViewContent:ExitFullScreen",
+ "GeckoView:ClearMatches",
+ "GeckoView:DisplayMatches",
+ "GeckoView:FindInPage",
+ "GeckoView:HasCookieBannerRuleForBrowsingContextTree",
+ "GeckoView:RestoreState",
+ "GeckoView:ContainsFormData",
+ "GeckoView:RequestCreateAnalysis",
+ "GeckoView:RequestAnalysisStatus",
+ "GeckoView:RequestAnalysisCreationStatus",
+ "GeckoView:PollForAnalysisCompleted",
+ "GeckoView:SendClickAttributionEvent",
+ "GeckoView:SendImpressionAttributionEvent",
+ "GeckoView:SendPlacementAttributionEvent",
+ "GeckoView:RequestAnalysis",
+ "GeckoView:RequestRecommendations",
+ "GeckoView:ReportBackInStock",
+ "GeckoView:ScrollBy",
+ "GeckoView:ScrollTo",
+ "GeckoView:SetActive",
+ "GeckoView:SetFocused",
+ "GeckoView:SetPriorityHint",
+ "GeckoView:UpdateInitData",
+ "GeckoView:ZoomToInput",
+ "GeckoView:IsPdfJs",
+ ]);
+ }
+
+ 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");
+ }
+
+ get isPdfJs() {
+ return (
+ this.browser.contentPrincipal.spec === "resource://pdf.js/web/viewer.html"
+ );
+ }
+
+ // 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;
+ }
+ }
+
+ #sendDOMFullScreenEventToAllChildren(aEvent) {
+ let { browsingContext } = this.actor;
+
+ while (browsingContext) {
+ if (!browsingContext.currentWindowGlobal) {
+ break;
+ }
+
+ const currentPid = browsingContext.currentWindowGlobal.osPid;
+ const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
+
+ if (currentPid != parentPid) {
+ if (!browsingContext.parent) {
+ // Top level browsing context. Use origin actor (Bug 1505916).
+ const chromeBC = browsingContext.topChromeWindow?.browsingContext;
+ const requestOrigin = chromeBC?.fullscreenRequestOrigin?.get();
+ if (requestOrigin) {
+ requestOrigin.browsingContext.currentWindowGlobal
+ .getActor("GeckoViewContent")
+ .sendAsyncMessage(aEvent, {});
+ delete chromeBC.fullscreenRequestOrigin;
+ return;
+ }
+ }
+ const actor =
+ browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
+ actor.sendAsyncMessage(aEvent, {});
+ }
+
+ 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": {
+ if (!this.isPdfJs) {
+ this._clearMatches();
+ }
+ break;
+ }
+ case "GeckoView:DisplayMatches": {
+ if (!this.isPdfJs) {
+ this._displayMatches(aData);
+ }
+ break;
+ }
+ case "GeckoView:FindInPage": {
+ if (!this.isPdfJs) {
+ this._findInPage(aData, aCallback);
+ }
+ break;
+ }
+ case "GeckoView:ZoomToInput":
+ // For ZoomToInput we just need to send the message to the current focused one.
+ const actor =
+ Services.focus.focusedContentBrowsingContext.currentWindowGlobal.getActor(
+ "GeckoViewContent"
+ );
+ actor.sendAsyncMessage(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;
+ case "GeckoView:RequestAnalysis":
+ this._requestAnalysis(aData, aCallback);
+ break;
+ case "GeckoView:RequestCreateAnalysis":
+ this._requestCreateAnalysis(aData, aCallback);
+ break;
+ case "GeckoView:RequestAnalysisStatus":
+ this._requestAnalysisStatus(aData, aCallback);
+ break;
+ case "GeckoView:RequestAnalysisCreationStatus":
+ this._requestAnalysisCreationStatus(aData, aCallback);
+ break;
+ case "GeckoView:PollForAnalysisCompleted":
+ this._pollForAnalysisCompleted(aData, aCallback);
+ break;
+ case "GeckoView:SendClickAttributionEvent":
+ this._sendAttributionEvent("click", aData, aCallback);
+ break;
+ case "GeckoView:SendImpressionAttributionEvent":
+ this._sendAttributionEvent("impression", aData, aCallback);
+ break;
+ case "GeckoView:SendPlacementAttributionEvent":
+ this._sendAttributionEvent("placement", aData, aCallback);
+ break;
+ case "GeckoView:RequestRecommendations":
+ this._requestRecommendations(aData, aCallback);
+ break;
+ case "GeckoView:ReportBackInStock":
+ this._reportBackInStock(aData, aCallback);
+ break;
+ case "GeckoView:IsPdfJs":
+ aCallback.onSuccess(this.isPdfJs);
+ break;
+ case "GeckoView:HasCookieBannerRuleForBrowsingContextTree":
+ this._hasCookieBannerRuleForBrowsingContextTree(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.#sendDOMFullScreenEventToAllChildren(
+ "GeckoView:DOMFullscreenEntered"
+ );
+ }
+ break;
+ case "MozDOMFullscreen:Exited":
+ this.#sendDOMFullScreenEventToAllChildren(
+ "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());
+ }
+
+ async _requestAnalysis(aData, aCallback) {
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ const analysis = {
+ analysis_url: "https://www.example.com/mock_analysis_url",
+ product_id: "ABCDEFG123",
+ grade: "B",
+ adjusted_rating: 4.5,
+ needs_analysis: true,
+ page_not_supported: true,
+ not_enough_reviews: true,
+ highlights: null,
+ last_analysis_time: 12345,
+ deleted_product_reported: true,
+ deleted_product: true,
+ };
+ aCallback.onSuccess({ analysis });
+ return;
+ }
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(`Cannot requestAnalysis on a non-product url.`);
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const analysis = await product.requestAnalysis();
+ if (!analysis) {
+ aCallback.onError(`Product analysis returned null.`);
+ return;
+ }
+ aCallback.onSuccess({ analysis });
+ }
+ }
+
+ async _requestCreateAnalysis(aData, aCallback) {
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ const status = "pending";
+ aCallback.onSuccess(status);
+ return;
+ }
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(`Cannot requestCreateAnalysis on a non-product url.`);
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const status = await product.requestCreateAnalysis();
+ if (!status) {
+ aCallback.onError(`Creation of product analysis returned null.`);
+ return;
+ }
+ aCallback.onSuccess(status.status);
+ }
+ }
+
+ async _requestAnalysisCreationStatus(aData, aCallback) {
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ const status = "in_progress";
+ aCallback.onSuccess(status);
+ return;
+ }
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(
+ `Cannot requestAnalysisCreationStatus on a non-product url.`
+ );
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const status = await product.requestAnalysisCreationStatus();
+ if (!status) {
+ aCallback.onError(
+ `Status of creation of product analysis returned null.`
+ );
+ return;
+ }
+ aCallback.onSuccess(status.status);
+ }
+ }
+
+ async _requestAnalysisStatus(aData, aCallback) {
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ const status = { status: "in_progress", progress: 90.9 };
+ aCallback.onSuccess({ status });
+ return;
+ }
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(`Cannot requestAnalysisStatus on a non-product url.`);
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const status = await product.requestAnalysisCreationStatus();
+ if (!status) {
+ aCallback.onError(`Status of product analysis returned null.`);
+ return;
+ }
+ aCallback.onSuccess({ status });
+ }
+ }
+
+ async _pollForAnalysisCompleted(aData, aCallback) {
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(
+ `Cannot pollForAnalysisCompleted on a non-product url.`
+ );
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const status = await product.pollForAnalysisCompleted();
+ if (!status) {
+ aCallback.onError(
+ `Polling the status of creation of product analysis returned null.`
+ );
+ return;
+ }
+ aCallback.onSuccess(status.status);
+ }
+ }
+
+ async _sendAttributionEvent(aEvent, aData, aCallback) {
+ let result;
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ result = { TEST_AID: "TEST_AID_RESPONSE" };
+ } else {
+ result = await lazy.ShoppingProduct.sendAttributionEvent(
+ aEvent,
+ aData.aid,
+ "geckoview_android"
+ );
+ }
+ if (!result || !(aData.aid in result) || !result[aData.aid]) {
+ aCallback.onSuccess(false);
+ return;
+ }
+ aCallback.onSuccess(true);
+ }
+
+ async _requestRecommendations(aData, aCallback) {
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ const recommendations = [
+ {
+ name: "Mock Product",
+ url: "https://example.com/mock_url",
+ image_url: "https://example.com/mock_image_url",
+ price: "450",
+ currency: "USD",
+ grade: "C",
+ adjusted_rating: 3.5,
+ analysis_url: "https://example.com/mock_analysis_url",
+ sponsored: true,
+ aid: "mock_aid",
+ },
+ ];
+ aCallback.onSuccess({ recommendations });
+ return;
+ }
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(`Cannot requestRecommendations on a non-product url.`);
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const recommendations = await product.requestRecommendations();
+ if (!recommendations) {
+ aCallback.onError(`Product recommendations returned null.`);
+ return;
+ }
+ aCallback.onSuccess({ recommendations });
+ }
+ }
+
+ async _reportBackInStock(aData, aCallback) {
+ if (
+ Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
+ ) {
+ const message = "report created";
+ aCallback.onSuccess(message);
+ return;
+ }
+ const url = Services.io.newURI(aData.url);
+ if (!lazy.isProductURL(url)) {
+ aCallback.onError(`Cannot reportBackInStock on a non-product url.`);
+ } else {
+ const product = new lazy.ShoppingProduct(url);
+ const message = await product.sendReport();
+ if (!message) {
+ aCallback.onError(`Reporting back in stock returned null.`);
+ return;
+ }
+ aCallback.onSuccess(message.message);
+ }
+ }
+
+ async _hasCookieBannerRuleForBrowsingContextTree(aCallback) {
+ const { browsingContext } = this.actor;
+ aCallback.onSuccess(
+ Services.cookieBanners.hasRuleForBrowsingContextTree(browsingContext)
+ );
+ }
+
+ _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");