Bug 1888861 - Wait for resize events in the parent process document for ZoomToFocusIn...
[gecko.git] / mobile / android / modules / geckoview / GeckoViewContent.sys.mjs
blob35ffb76d90bec01d9f14b3e69ff0f0ac20290af7
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
7 const lazy = {};
8 ChromeUtils.defineESModuleGetters(lazy, {
9   isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
10   ShoppingProduct: "chrome://global/content/shopping/ShoppingProduct.mjs",
11 });
13 export class GeckoViewContent extends GeckoViewModule {
14   onInit() {
15     this.registerListener([
16       "GeckoViewContent:ExitFullScreen",
17       "GeckoView:ClearMatches",
18       "GeckoView:DisplayMatches",
19       "GeckoView:FindInPage",
20       "GeckoView:HasCookieBannerRuleForBrowsingContextTree",
21       "GeckoView:RestoreState",
22       "GeckoView:ContainsFormData",
23       "GeckoView:RequestCreateAnalysis",
24       "GeckoView:RequestAnalysisStatus",
25       "GeckoView:RequestAnalysisCreationStatus",
26       "GeckoView:PollForAnalysisCompleted",
27       "GeckoView:SendClickAttributionEvent",
28       "GeckoView:SendImpressionAttributionEvent",
29       "GeckoView:SendPlacementAttributionEvent",
30       "GeckoView:RequestAnalysis",
31       "GeckoView:RequestRecommendations",
32       "GeckoView:ReportBackInStock",
33       "GeckoView:ScrollBy",
34       "GeckoView:ScrollTo",
35       "GeckoView:SetActive",
36       "GeckoView:SetFocused",
37       "GeckoView:SetPriorityHint",
38       "GeckoView:UpdateInitData",
39       "GeckoView:ZoomToInput",
40       "GeckoView:IsPdfJs",
41     ]);
42   }
44   onEnable() {
45     this.window.addEventListener(
46       "MozDOMFullscreen:Entered",
47       this,
48       /* capture */ true,
49       /* untrusted */ false
50     );
51     this.window.addEventListener(
52       "MozDOMFullscreen:Exited",
53       this,
54       /* capture */ true,
55       /* untrusted */ false
56     );
57     this.window.addEventListener(
58       "framefocusrequested",
59       this,
60       /* capture */ true,
61       /* untrusted */ false
62     );
64     this.window.addEventListener("DOMWindowClose", this);
65     this.window.addEventListener("pagetitlechanged", this);
66     this.window.addEventListener("pageinfo", this);
68     this.window.addEventListener("cookiebannerdetected", this);
69     this.window.addEventListener("cookiebannerhandled", this);
71     Services.obs.addObserver(this, "oop-frameloader-crashed");
72     Services.obs.addObserver(this, "ipc:content-shutdown");
73   }
75   onDisable() {
76     this.window.removeEventListener(
77       "MozDOMFullscreen:Entered",
78       this,
79       /* capture */ true
80     );
81     this.window.removeEventListener(
82       "MozDOMFullscreen:Exited",
83       this,
84       /* capture */ true
85     );
86     this.window.removeEventListener(
87       "framefocusrequested",
88       this,
89       /* capture */ true
90     );
92     this.window.removeEventListener("DOMWindowClose", this);
93     this.window.removeEventListener("pagetitlechanged", this);
94     this.window.removeEventListener("pageinfo", this);
96     this.window.removeEventListener("cookiebannerdetected", this);
97     this.window.removeEventListener("cookiebannerhandled", this);
99     Services.obs.removeObserver(this, "oop-frameloader-crashed");
100     Services.obs.removeObserver(this, "ipc:content-shutdown");
101   }
103   get actor() {
104     return this.getActor("GeckoViewContent");
105   }
107   get isPdfJs() {
108     return (
109       this.browser.contentPrincipal.spec === "resource://pdf.js/web/viewer.html"
110     );
111   }
113   // Goes up the browsingContext chain and sends the message every time
114   // we cross the process boundary so that every process in the chain is
115   // notified.
116   sendToAllChildren(aEvent, aData) {
117     let { browsingContext } = this.actor;
119     while (browsingContext) {
120       if (!browsingContext.currentWindowGlobal) {
121         break;
122       }
124       const currentPid = browsingContext.currentWindowGlobal.osPid;
125       const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
127       if (currentPid != parentPid) {
128         const actor =
129           browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
130         actor.sendAsyncMessage(aEvent, aData);
131       }
133       browsingContext = browsingContext.parent;
134     }
135   }
137   #sendDOMFullScreenEventToAllChildren(aEvent) {
138     let { browsingContext } = this.actor;
140     while (browsingContext) {
141       if (!browsingContext.currentWindowGlobal) {
142         break;
143       }
145       const currentPid = browsingContext.currentWindowGlobal.osPid;
146       const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
148       if (currentPid != parentPid) {
149         if (!browsingContext.parent) {
150           // Top level browsing context. Use origin actor (Bug 1505916).
151           const chromeBC = browsingContext.topChromeWindow?.browsingContext;
152           const requestOrigin = chromeBC?.fullscreenRequestOrigin?.get();
153           if (requestOrigin) {
154             requestOrigin.browsingContext.currentWindowGlobal
155               .getActor("GeckoViewContent")
156               .sendAsyncMessage(aEvent, {});
157             delete chromeBC.fullscreenRequestOrigin;
158             return;
159           }
160         }
161         const actor =
162           browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
163         actor.sendAsyncMessage(aEvent, {});
164       }
166       browsingContext = browsingContext.parent;
167     }
168   }
170   // Bundle event handler.
171   onEvent(aEvent, aData, aCallback) {
172     debug`onEvent: event=${aEvent}, data=${aData}`;
174     switch (aEvent) {
175       case "GeckoViewContent:ExitFullScreen":
176         this.browser.ownerDocument.exitFullscreen();
177         break;
178       case "GeckoView:ClearMatches": {
179         if (!this.isPdfJs) {
180           this._clearMatches();
181         }
182         break;
183       }
184       case "GeckoView:DisplayMatches": {
185         if (!this.isPdfJs) {
186           this._displayMatches(aData);
187         }
188         break;
189       }
190       case "GeckoView:FindInPage": {
191         if (!this.isPdfJs) {
192           this._findInPage(aData, aCallback);
193         }
194         break;
195       }
196       case "GeckoView:ZoomToInput": {
197         const sendZoomToFocusedInputMessage = function () {
198           // For ZoomToInput we just need to send the message to the current focused one.
199           const actor =
200             Services.focus.focusedContentBrowsingContext.currentWindowGlobal.getActor(
201               "GeckoViewContent"
202             );
204           actor.sendAsyncMessage(aEvent, aData);
205         };
207         const { force } = aData;
208         let gotResize = false;
209         const onResize = function () {
210           gotResize = true;
211           if (this.window.windowUtils.isMozAfterPaintPending) {
212             this.window.addEventListener(
213               "MozAfterPaint",
214               () => sendZoomToFocusedInputMessage(),
215               { capture: true, once: true }
216             );
217           } else {
218             sendZoomToFocusedInputMessage();
219           }
220         };
222         this.window.addEventListener("resize", onResize, { capture: true });
224         // When the keyboard is displayed, we can get one resize event,
225         // multiple resize events, or none at all. Try to handle all these
226         // cases by allowing resizing within a set interval, and still zoom to
227         // input if there is no resize event at the end of the interval.
228         this.window.setTimeout(() => {
229           this.window.removeEventListener("resize", onResize, {
230             capture: true,
231           });
232           if (!gotResize && force) {
233             onResize();
234           }
235         }, 500);
236         break;
237       }
238       case "GeckoView:ScrollBy":
239         // Unclear if that actually works with oop iframes?
240         this.sendToAllChildren(aEvent, aData);
241         break;
242       case "GeckoView:ScrollTo":
243         // Unclear if that actually works with oop iframes?
244         this.sendToAllChildren(aEvent, aData);
245         break;
246       case "GeckoView:UpdateInitData":
247         this.sendToAllChildren(aEvent, aData);
248         break;
249       case "GeckoView:SetActive":
250         this.browser.docShellIsActive = !!aData.active;
251         break;
252       case "GeckoView:SetFocused":
253         if (aData.focused) {
254           this.browser.focus();
255           this.browser.setAttribute("primary", "true");
256         } else {
257           this.browser.removeAttribute("primary");
258           this.browser.blur();
259         }
260         break;
261       case "GeckoView:SetPriorityHint":
262         if (this.browser.isRemoteBrowser) {
263           const remoteTab = this.browser.frameLoader?.remoteTab;
264           if (remoteTab) {
265             remoteTab.priorityHint = aData.priorityHint;
266           }
267         }
268         break;
269       case "GeckoView:RestoreState":
270         this.actor.restoreState(aData);
271         break;
272       case "GeckoView:ContainsFormData":
273         this._containsFormData(aCallback);
274         break;
275       case "GeckoView:RequestAnalysis":
276         this._requestAnalysis(aData, aCallback);
277         break;
278       case "GeckoView:RequestCreateAnalysis":
279         this._requestCreateAnalysis(aData, aCallback);
280         break;
281       case "GeckoView:RequestAnalysisStatus":
282         this._requestAnalysisStatus(aData, aCallback);
283         break;
284       case "GeckoView:RequestAnalysisCreationStatus":
285         this._requestAnalysisCreationStatus(aData, aCallback);
286         break;
287       case "GeckoView:PollForAnalysisCompleted":
288         this._pollForAnalysisCompleted(aData, aCallback);
289         break;
290       case "GeckoView:SendClickAttributionEvent":
291         this._sendAttributionEvent("click", aData, aCallback);
292         break;
293       case "GeckoView:SendImpressionAttributionEvent":
294         this._sendAttributionEvent("impression", aData, aCallback);
295         break;
296       case "GeckoView:SendPlacementAttributionEvent":
297         this._sendAttributionEvent("placement", aData, aCallback);
298         break;
299       case "GeckoView:RequestRecommendations":
300         this._requestRecommendations(aData, aCallback);
301         break;
302       case "GeckoView:ReportBackInStock":
303         this._reportBackInStock(aData, aCallback);
304         break;
305       case "GeckoView:IsPdfJs":
306         aCallback.onSuccess(this.isPdfJs);
307         break;
308       case "GeckoView:HasCookieBannerRuleForBrowsingContextTree":
309         this._hasCookieBannerRuleForBrowsingContextTree(aCallback);
310         break;
311     }
312   }
314   // DOM event handler
315   handleEvent(aEvent) {
316     debug`handleEvent: ${aEvent.type}`;
318     switch (aEvent.type) {
319       case "framefocusrequested":
320         if (this.browser != aEvent.target) {
321           return;
322         }
323         if (this.browser.hasAttribute("primary")) {
324           return;
325         }
326         this.eventDispatcher.sendRequest({
327           type: "GeckoView:FocusRequest",
328         });
329         aEvent.preventDefault();
330         break;
331       case "MozDOMFullscreen:Entered":
332         if (this.browser == aEvent.target) {
333           // Remote browser; dispatch to content process.
334           this.#sendDOMFullScreenEventToAllChildren(
335             "GeckoView:DOMFullscreenEntered"
336           );
337         }
338         break;
339       case "MozDOMFullscreen:Exited":
340         this.#sendDOMFullScreenEventToAllChildren(
341           "GeckoView:DOMFullscreenExited"
342         );
343         break;
344       case "pagetitlechanged":
345         this.eventDispatcher.sendRequest({
346           type: "GeckoView:PageTitleChanged",
347           title: this.browser.contentTitle,
348         });
349         break;
350       case "DOMWindowClose":
351         // We need this because we want to allow the app
352         // to close the window itself. If we don't preventDefault()
353         // here Gecko will close it immediately.
354         aEvent.preventDefault();
356         this.eventDispatcher.sendRequest({
357           type: "GeckoView:DOMWindowClose",
358         });
359         break;
360       case "pageinfo":
361         if (aEvent.detail.previewImageURL) {
362           this.eventDispatcher.sendRequest({
363             type: "GeckoView:PreviewImage",
364             previewImageUrl: aEvent.detail.previewImageURL,
365           });
366         }
367         break;
368       case "cookiebannerdetected":
369         this.eventDispatcher.sendRequest({
370           type: "GeckoView:CookieBannerEvent:Detected",
371         });
372         break;
373       case "cookiebannerhandled":
374         this.eventDispatcher.sendRequest({
375           type: "GeckoView:CookieBannerEvent:Handled",
376         });
377         break;
378     }
379   }
381   // nsIObserver event handler
382   observe(aSubject, aTopic) {
383     debug`observe: ${aTopic}`;
384     this._contentCrashed = false;
385     const browser = aSubject.ownerElement;
387     switch (aTopic) {
388       case "oop-frameloader-crashed": {
389         if (!browser || browser != this.browser) {
390           return;
391         }
392         this.window.setTimeout(() => {
393           if (this._contentCrashed) {
394             this.eventDispatcher.sendRequest({
395               type: "GeckoView:ContentCrash",
396             });
397           } else {
398             this.eventDispatcher.sendRequest({
399               type: "GeckoView:ContentKill",
400             });
401           }
402         }, 250);
403         break;
404       }
405       case "ipc:content-shutdown": {
406         aSubject.QueryInterface(Ci.nsIPropertyBag2);
407         if (aSubject.get("dumpID")) {
408           if (
409             browser &&
410             aSubject.get("childID") != browser.frameLoader.childID
411           ) {
412             return;
413           }
414           this._contentCrashed = true;
415         }
416         break;
417       }
418     }
419   }
421   async _containsFormData(aCallback) {
422     aCallback.onSuccess(await this.actor.containsFormData());
423   }
425   async _requestAnalysis(aData, aCallback) {
426     if (
427       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
428     ) {
429       const analysis = {
430         analysis_url: "https://www.example.com/mock_analysis_url",
431         product_id: "ABCDEFG123",
432         grade: "B",
433         adjusted_rating: 4.5,
434         needs_analysis: true,
435         page_not_supported: true,
436         not_enough_reviews: true,
437         highlights: null,
438         last_analysis_time: 12345,
439         deleted_product_reported: true,
440         deleted_product: true,
441       };
442       aCallback.onSuccess({ analysis });
443       return;
444     }
445     const url = Services.io.newURI(aData.url);
446     if (!lazy.isProductURL(url)) {
447       aCallback.onError(`Cannot requestAnalysis on a non-product url.`);
448     } else {
449       const product = new lazy.ShoppingProduct(url);
450       const analysis = await product.requestAnalysis();
451       if (!analysis) {
452         aCallback.onError(`Product analysis returned null.`);
453         return;
454       }
455       aCallback.onSuccess({ analysis });
456     }
457   }
459   async _requestCreateAnalysis(aData, aCallback) {
460     if (
461       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
462     ) {
463       const status = "pending";
464       aCallback.onSuccess(status);
465       return;
466     }
467     const url = Services.io.newURI(aData.url);
468     if (!lazy.isProductURL(url)) {
469       aCallback.onError(`Cannot requestCreateAnalysis on a non-product url.`);
470     } else {
471       const product = new lazy.ShoppingProduct(url);
472       const status = await product.requestCreateAnalysis();
473       if (!status) {
474         aCallback.onError(`Creation of product analysis returned null.`);
475         return;
476       }
477       aCallback.onSuccess(status.status);
478     }
479   }
481   async _requestAnalysisCreationStatus(aData, aCallback) {
482     if (
483       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
484     ) {
485       const status = "in_progress";
486       aCallback.onSuccess(status);
487       return;
488     }
489     const url = Services.io.newURI(aData.url);
490     if (!lazy.isProductURL(url)) {
491       aCallback.onError(
492         `Cannot requestAnalysisCreationStatus on a non-product url.`
493       );
494     } else {
495       const product = new lazy.ShoppingProduct(url);
496       const status = await product.requestAnalysisCreationStatus();
497       if (!status) {
498         aCallback.onError(
499           `Status of creation of product analysis returned null.`
500         );
501         return;
502       }
503       aCallback.onSuccess(status.status);
504     }
505   }
507   async _requestAnalysisStatus(aData, aCallback) {
508     if (
509       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
510     ) {
511       const status = { status: "in_progress", progress: 90.9 };
512       aCallback.onSuccess({ status });
513       return;
514     }
515     const url = Services.io.newURI(aData.url);
516     if (!lazy.isProductURL(url)) {
517       aCallback.onError(`Cannot requestAnalysisStatus on a non-product url.`);
518     } else {
519       const product = new lazy.ShoppingProduct(url);
520       const status = await product.requestAnalysisCreationStatus();
521       if (!status) {
522         aCallback.onError(`Status of product analysis returned null.`);
523         return;
524       }
525       aCallback.onSuccess({ status });
526     }
527   }
529   async _pollForAnalysisCompleted(aData, aCallback) {
530     const url = Services.io.newURI(aData.url);
531     if (!lazy.isProductURL(url)) {
532       aCallback.onError(
533         `Cannot pollForAnalysisCompleted on a non-product url.`
534       );
535     } else {
536       const product = new lazy.ShoppingProduct(url);
537       const status = await product.pollForAnalysisCompleted();
538       if (!status) {
539         aCallback.onError(
540           `Polling the status of creation of product analysis returned null.`
541         );
542         return;
543       }
544       aCallback.onSuccess(status.status);
545     }
546   }
548   async _sendAttributionEvent(aEvent, aData, aCallback) {
549     let result;
550     if (
551       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
552     ) {
553       result = { TEST_AID: "TEST_AID_RESPONSE" };
554     } else {
555       result = await lazy.ShoppingProduct.sendAttributionEvent(
556         aEvent,
557         aData.aid,
558         "geckoview_android"
559       );
560     }
561     if (!result || !(aData.aid in result) || !result[aData.aid]) {
562       aCallback.onSuccess(false);
563       return;
564     }
565     aCallback.onSuccess(true);
566   }
568   async _requestRecommendations(aData, aCallback) {
569     if (
570       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
571     ) {
572       const recommendations = [
573         {
574           name: "Mock Product",
575           url: "https://example.com/mock_url",
576           image_url: "https://example.com/mock_image_url",
577           price: "450",
578           currency: "USD",
579           grade: "C",
580           adjusted_rating: 3.5,
581           analysis_url: "https://example.com/mock_analysis_url",
582           sponsored: true,
583           aid: "mock_aid",
584         },
585       ];
586       aCallback.onSuccess({ recommendations });
587       return;
588     }
589     const url = Services.io.newURI(aData.url);
590     if (!lazy.isProductURL(url)) {
591       aCallback.onError(`Cannot requestRecommendations on a non-product url.`);
592     } else {
593       const product = new lazy.ShoppingProduct(url);
594       const recommendations = await product.requestRecommendations();
595       if (!recommendations) {
596         aCallback.onError(`Product recommendations returned null.`);
597         return;
598       }
599       aCallback.onSuccess({ recommendations });
600     }
601   }
603   async _reportBackInStock(aData, aCallback) {
604     if (
605       Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
606     ) {
607       const message = "report created";
608       aCallback.onSuccess(message);
609       return;
610     }
611     const url = Services.io.newURI(aData.url);
612     if (!lazy.isProductURL(url)) {
613       aCallback.onError(`Cannot reportBackInStock on a non-product url.`);
614     } else {
615       const product = new lazy.ShoppingProduct(url);
616       const message = await product.sendReport();
617       if (!message) {
618         aCallback.onError(`Reporting back in stock returned null.`);
619         return;
620       }
621       aCallback.onSuccess(message.message);
622     }
623   }
625   async _hasCookieBannerRuleForBrowsingContextTree(aCallback) {
626     const { browsingContext } = this.actor;
627     aCallback.onSuccess(
628       Services.cookieBanners.hasRuleForBrowsingContextTree(browsingContext)
629     );
630   }
632   _findInPage(aData, aCallback) {
633     debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`;
635     let finder;
636     try {
637       finder = this.browser.finder;
638     } catch (e) {
639       if (aCallback) {
640         aCallback.onError(`No finder: ${e}`);
641       }
642       return;
643     }
645     if (this._finderListener) {
646       finder.removeResultListener(this._finderListener);
647     }
649     this._finderListener = {
650       response: {
651         found: false,
652         wrapped: false,
653         current: 0,
654         total: -1,
655         searchString: aData.searchString || finder.searchString,
656         linkURL: null,
657         clientRect: null,
658         flags: {
659           backwards: !!aData.backwards,
660           linksOnly: !!aData.linksOnly,
661           matchCase: !!aData.matchCase,
662           wholeWord: !!aData.wholeWord,
663         },
664       },
666       onFindResult(aOptions) {
667         if (!aCallback || aOptions.searchString !== aData.searchString) {
668           // Result from a previous search.
669           return;
670         }
672         Object.assign(this.response, {
673           found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND,
674           wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND,
675           linkURL: aOptions.linkURL,
676           clientRect: aOptions.rect && {
677             left: aOptions.rect.left,
678             top: aOptions.rect.top,
679             right: aOptions.rect.right,
680             bottom: aOptions.rect.bottom,
681           },
682           flags: {
683             backwards: aOptions.findBackwards,
684             linksOnly: aOptions.linksOnly,
685             matchCase: this.response.flags.matchCase,
686             wholeWord: this.response.flags.wholeWord,
687           },
688         });
690         if (!this.response.found) {
691           this.response.current = 0;
692           this.response.total = 0;
693         }
695         // Only send response if we have a count.
696         if (!this.response.found || this.response.current !== 0) {
697           debug`onFindResult: ${this.response}`;
698           aCallback.onSuccess(this.response);
699           aCallback = undefined;
700         }
701       },
703       onMatchesCountResult(aResult) {
704         if (!aCallback || finder.searchString !== aData.searchString) {
705           // Result from a previous search.
706           return;
707         }
709         Object.assign(this.response, {
710           current: aResult.current,
711           total: aResult.total,
712         });
714         // Only send response if we have a result. `found` and `wrapped` are
715         // both false only when we haven't received a result yet.
716         if (this.response.found || this.response.wrapped) {
717           debug`onMatchesCountResult: ${this.response}`;
718           aCallback.onSuccess(this.response);
719           aCallback = undefined;
720         }
721       },
723       onCurrentSelection() {},
725       onHighlightFinished() {},
726     };
728     finder.caseSensitive = !!aData.matchCase;
729     finder.entireWord = !!aData.wholeWord;
730     finder.matchDiacritics = !!aData.matchDiacritics;
731     finder.addResultListener(this._finderListener);
733     const drawOutline =
734       this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline;
736     if (!aData.searchString || aData.searchString === finder.searchString) {
737       // Search again.
738       aData.searchString = finder.searchString;
739       finder.findAgain(
740         aData.searchString,
741         !!aData.backwards,
742         !!aData.linksOnly,
743         drawOutline
744       );
745     } else {
746       finder.fastFind(aData.searchString, !!aData.linksOnly, drawOutline);
747     }
748   }
750   _clearMatches() {
751     debug`clearMatches`;
753     let finder;
754     try {
755       finder = this.browser.finder;
756     } catch (e) {
757       return;
758     }
760     finder.removeSelection();
761     finder.highlight(false);
763     if (this._finderListener) {
764       finder.removeResultListener(this._finderListener);
765       this._finderListener = null;
766     }
767   }
769   _displayMatches(aData) {
770     debug`displayMatches: data=${aData}`;
772     let finder;
773     try {
774       finder = this.browser.finder;
775     } catch (e) {
776       return;
777     }
779     this._matchDisplayOptions = aData;
780     finder.onModalHighlightChange(!!aData.dimPage);
781     finder.onHighlightAllChange(!!aData.highlightAll);
783     if (!aData.highlightAll && !aData.dimPage) {
784       finder.highlight(false);
785       return;
786     }
788     if (!this._finderListener || !finder.searchString) {
789       return;
790     }
791     const linksOnly = this._finderListener.response.linksOnly;
792     finder.highlight(true, finder.searchString, linksOnly, !!aData.drawOutline);
793   }
796 const { debug, warn } = GeckoViewContent.initLogging("GeckoViewContent");