Backed out 7 changesets (bug 1942424) for causing frequent crashes. a=backout
[gecko.git] / toolkit / components / pdfjs / content / GeckoViewPdfjsParent.sys.mjs
blobc97a5bb7c979ac55cb4b41226e1ed8b9cf682c3e
1 /* Copyright 2022 Mozilla Foundation
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
16 import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs";
18 const lazy = {};
19 ChromeUtils.defineESModuleGetters(lazy, {
20   PdfJsTelemetry: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
21   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
22 });
24 class FindHandler {
25   #browser;
27   #callbacks = null;
29   #state = null;
31   constructor(aBrowser) {
32     this.#browser = aBrowser;
33   }
35   cleanup() {
36     this.#state = null;
37     this.#callbacks = null;
38   }
40   onEvent(aEvent, aData, aCallback) {
41     if (
42       this.#browser.contentPrincipal.spec !==
43       "resource://pdf.js/web/viewer.html"
44     ) {
45       return;
46     }
48     debug`onEvent: name=${aEvent}, data=${aData}`;
50     switch (aEvent) {
51       case "GeckoView:ClearMatches":
52         this.cleanup();
53         this.#browser.sendMessageToActor(
54           "PDFJS:Child:handleEvent",
55           {
56             type: "findbarclose",
57             detail: null,
58           },
59           "GeckoViewPdfjs"
60         );
61         break;
62       case "GeckoView:DisplayMatches":
63         if (!this.#state) {
64           return;
65         }
66         this.#browser.sendMessageToActor(
67           "PDFJS:Child:handleEvent",
68           {
69             type: "findhighlightallchange",
70             detail: this.#state,
71           },
72           "GeckoViewPdfjs"
73         );
74         break;
75       case "GeckoView:FindInPage":
76         const type = this.#getFindType(aData);
77         this.#browser.sendMessageToActor(
78           "PDFJS:Child:handleEvent",
79           {
80             type,
81             detail: this.#state,
82           },
83           "GeckoViewPdfjs"
84         );
85         this.#callbacks.push([aCallback, this.#state]);
86         break;
87     }
88   }
90   #getFindType(aData) {
91     const newState = {
92       query: this.#state?.query,
93       caseSensitive: !!aData.matchCase,
94       entireWord: !!aData.wholeWord,
95       highlightAll: !!aData.highlightAll,
96       findPrevious: !!aData.backwards,
97       matchDiacritics: !!aData.matchDiacritics,
98     };
99     if (!this.#state) {
100       // It's a new search.
101       newState.query = aData.searchString;
102       this.#state = newState;
103       this.#callbacks = [];
104       return "find";
105     }
107     if (aData.searchString && this.#state.query !== aData.searchString) {
108       // The searched string has changed.
109       newState.query = aData.searchString;
110       this.#state = newState;
111       return "find";
112     }
114     for (const [key, type] of [
115       ["caseSensitive", "findcasesensitivitychange"],
116       ["entireWord", "findentirewordchange"],
117       ["matchDiacritics", "finddiacriticmatchingchange"],
118     ]) {
119       if (this.#state[key] !== newState[key]) {
120         this.#state = newState;
121         return type;
122       }
123     }
125     this.#state = newState;
126     return "findagain";
127   }
129   updateMatchesCount(aData, aResult) {
130     if (
131       (aResult !== Ci.nsITypeAheadFind.FIND_FOUND &&
132         aResult !== Ci.nsITypeAheadFind.FIND_WRAPPED) ||
133       !this.#state
134     ) {
135       return;
136     }
138     if (this.#callbacks.length === 0) {
139       warn`There are no callbacks to use to set the matches count.`;
140       return;
141     }
143     const [callback, state] = this.#callbacks.shift();
145     aData ||= { current: 0, total: -1 };
147     const response = {
148       found: aData.total !== 0,
149       wrapped:
150         aData.total === 0 || aResult === Ci.nsITypeAheadFind.FIND_WRAPPED,
151       current: aData.current,
152       total: aData.total,
153       searchString: state.query,
154       linkURL: null,
155       clientRect: null,
156       flags: {
157         backwards: state.findPrevious,
158         matchCase: state.caseSensitive,
159         wholeWord: state.entireWord,
160       },
161     };
162     callback.onSuccess(response);
163   }
166 class FileSaver {
167   #browser;
169   #callback = null;
171   #eventDispatcher;
173   constructor(aBrowser, eventDispatcher) {
174     this.#browser = aBrowser;
175     this.#eventDispatcher = eventDispatcher;
176   }
178   cleanup() {
179     this.#callback = null;
180   }
182   onEvent(aEvent, aData, aCallback) {
183     if (
184       this.#browser.contentPrincipal.spec !==
185       "resource://pdf.js/web/viewer.html"
186     ) {
187       return;
188     }
190     lazy.PdfJsTelemetry.onGeckoview("save_as_pdf_tapped");
192     this.#callback = aCallback;
193     this.#browser.sendMessageToActor(
194       "PDFJS:Child:handleEvent",
195       {
196         type: "save",
197       },
198       "GeckoViewPdfjs"
199     );
200   }
202   async save({ blobUrl, filename, originalUrl }) {
203     try {
204       const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
205         this.#browser
206       );
208       if (this.#callback) {
209         // "Save as PDF" from the share menu.
210         this.#callback.onSuccess({
211           url: blobUrl,
212           filename,
213           originalUrl,
214           isPrivate,
215         });
216       } else {
217         // "Download" or "Open in app" from the pdf.js toolbar.
218         this.#eventDispatcher.sendRequest({
219           type: "GeckoView:SavePdf",
220           url: blobUrl,
221           filename,
222           originalUrl,
223           isPrivate,
224           skipConfirmation: true,
225           requestExternalApp: false,
226         });
227       }
228       lazy.PdfJsTelemetry.onGeckoview("download_succeeded");
229     } catch (e) {
230       lazy.PdfJsTelemetry.onGeckoview("download_failed");
231       if (this.#callback) {
232         this.#callback?.onError(`Cannot save the pdf: ${e}.`);
233       } else {
234         warn`Cannot save the pdf because of: ${e}`;
235       }
236     } finally {
237       this.cleanup();
238     }
239   }
242 export class GeckoViewPdfjsParent extends GeckoViewActorParent {
243   #findHandler;
245   #fileSaver;
247   receiveMessage(aMsg) {
248     debug`receiveMessage: name=${aMsg.name}, data=${aMsg.data}`;
250     switch (aMsg.name) {
251       case "PDFJS:Parent:updateControlState":
252         return this.#updateControlState(aMsg);
253       case "PDFJS:Parent:updateMatchesCount":
254         return this.#updateMatchesCount(aMsg);
255       case "PDFJS:Parent:addEventListener":
256         return this.#addEventListener(aMsg);
257       case "PDFJS:Parent:saveURL":
258         return this.#save(aMsg);
259       case "PDFJS:Parent:getNimbus":
260         return this.#getExperimentFeature();
261       case "PDFJS:Parent:recordExposure":
262         return this.#recordExposure();
263       case "PDFJS:Parent:reportTelemetry":
264         return this.#reportTelemetry(aMsg);
265       default:
266         break;
267     }
269     return undefined;
270   }
272   didDestroy() {
273     debug`didDestroy`;
275     if (!this.#findHandler) {
276       return;
277     }
279     // The eventDispatcher is null when the tab is destroyed.
280     if (this.eventDispatcher) {
281       this.eventDispatcher.unregisterListener(this.#findHandler, [
282         "GeckoView:ClearMatches",
283         "GeckoView:DisplayMatches",
284         "GeckoView:FindInPage",
285       ]);
286       this.eventDispatcher.unregisterListener(this.#fileSaver, [
287         "GeckoView:PDFSave",
288       ]);
289     }
291     this.#findHandler.cleanup();
292     this.#findHandler = null;
293     this.#fileSaver.cleanup();
294     this.#fileSaver = null;
295   }
297   #addEventListener({ data: { aSupportsFind } }) {
298     this.#fileSaver = new FileSaver(this.browser, this.eventDispatcher);
299     this.eventDispatcher.registerListener(this.#fileSaver, [
300       "GeckoView:PDFSave",
301     ]);
303     if (!aSupportsFind) {
304       return;
305     }
307     if (this.#findHandler) {
308       this.#findHandler.cleanup();
309       return;
310     }
312     this.#findHandler = new FindHandler(this.browser);
313     this.eventDispatcher.registerListener(this.#findHandler, [
314       "GeckoView:ClearMatches",
315       "GeckoView:DisplayMatches",
316       "GeckoView:FindInPage",
317     ]);
318   }
320   #updateMatchesCount({ data }) {
321     this.#findHandler.updateMatchesCount(data, Ci.nsITypeAheadFind.FIND_FOUND);
322   }
324   #updateControlState({ data: { matchesCount, result } }) {
325     this.#findHandler.updateMatchesCount(matchesCount, result);
326   }
328   #save({ data }) {
329     this.#fileSaver.save(data);
330   }
332   async #getExperimentFeature() {
333     let result = null;
334     try {
335       const experimentActor = this.window.moduleManager.getActor(
336         "GeckoViewExperimentDelegate"
337       );
338       result = await experimentActor.getExperimentFeature("pdfjs");
339     } catch (e) {
340       warn`Cannot get experiment feature: ${e}`;
341     }
342     this.sendAsyncMessage("PDFJS:Child:getNimbus", result);
343   }
345   async #recordExposure() {
346     try {
347       const experimentActor = this.window.moduleManager.getActor(
348         "GeckoViewExperimentDelegate"
349       );
350       await experimentActor.recordExposure("pdfjs");
351     } catch (e) {
352       warn`Cannot record experiment exposure: ${e}`;
353     }
354   }
356   #reportTelemetry(aMsg) {
357     lazy.PdfJsTelemetry.report(aMsg.data);
358   }
361 const { debug, warn } = GeckoViewPdfjsParent.initLogging(
362   "GeckoViewPdfjsParent"