diff options
Diffstat (limited to 'toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs')
-rw-r--r-- | toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs b/toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs new file mode 100644 index 0000000000..b07ed8c7b1 --- /dev/null +++ b/toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs @@ -0,0 +1,364 @@ +/* Copyright 2022 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + PdfJsTelemetry: "resource://pdf.js/PdfJsTelemetry.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +class FindHandler { + #browser; + + #callbacks = null; + + #state = null; + + constructor(aBrowser) { + this.#browser = aBrowser; + } + + cleanup() { + this.#state = null; + this.#callbacks = null; + } + + onEvent(aEvent, aData, aCallback) { + if ( + this.#browser.contentPrincipal.spec !== + "resource://pdf.js/web/viewer.html" + ) { + return; + } + + debug`onEvent: name=${aEvent}, data=${aData}`; + + switch (aEvent) { + case "GeckoView:ClearMatches": + this.cleanup(); + this.#browser.sendMessageToActor( + "PDFJS:Child:handleEvent", + { + type: "findbarclose", + detail: null, + }, + "GeckoViewPdfjs" + ); + break; + case "GeckoView:DisplayMatches": + if (!this.#state) { + return; + } + this.#browser.sendMessageToActor( + "PDFJS:Child:handleEvent", + { + type: "findhighlightallchange", + detail: this.#state, + }, + "GeckoViewPdfjs" + ); + break; + case "GeckoView:FindInPage": + const type = this.#getFindType(aData); + this.#browser.sendMessageToActor( + "PDFJS:Child:handleEvent", + { + type, + detail: this.#state, + }, + "GeckoViewPdfjs" + ); + this.#callbacks.push([aCallback, this.#state]); + break; + } + } + + #getFindType(aData) { + const newState = { + query: this.#state?.query, + caseSensitive: !!aData.matchCase, + entireWord: !!aData.wholeWord, + highlightAll: !!aData.highlightAll, + findPrevious: !!aData.backwards, + matchDiacritics: !!aData.matchDiacritics, + }; + if (!this.#state) { + // It's a new search. + newState.query = aData.searchString; + this.#state = newState; + this.#callbacks = []; + return "find"; + } + + if (aData.searchString && this.#state.query !== aData.searchString) { + // The searched string has changed. + newState.query = aData.searchString; + this.#state = newState; + return "find"; + } + + for (const [key, type] of [ + ["caseSensitive", "findcasesensitivitychange"], + ["entireWord", "findentirewordchange"], + ["matchDiacritics", "finddiacriticmatchingchange"], + ]) { + if (this.#state[key] !== newState[key]) { + this.#state = newState; + return type; + } + } + + this.#state = newState; + return "findagain"; + } + + updateMatchesCount(aData, aResult) { + if ( + (aResult !== Ci.nsITypeAheadFind.FIND_FOUND && + aResult !== Ci.nsITypeAheadFind.FIND_WRAPPED) || + !this.#state + ) { + return; + } + + if (this.#callbacks.length === 0) { + warn`There are no callbacks to use to set the matches count.`; + return; + } + + const [callback, state] = this.#callbacks.shift(); + + aData ||= { current: 0, total: -1 }; + + const response = { + found: aData.total !== 0, + wrapped: + aData.total === 0 || aResult === Ci.nsITypeAheadFind.FIND_WRAPPED, + current: aData.current, + total: aData.total, + searchString: state.query, + linkURL: null, + clientRect: null, + flags: { + backwards: state.findPrevious, + matchCase: state.caseSensitive, + wholeWord: state.entireWord, + }, + }; + callback.onSuccess(response); + } +} + +class FileSaver { + #browser; + + #callback = null; + + #eventDispatcher; + + constructor(aBrowser, eventDispatcher) { + this.#browser = aBrowser; + this.#eventDispatcher = eventDispatcher; + } + + cleanup() { + this.#callback = null; + } + + onEvent(aEvent, aData, aCallback) { + if ( + this.#browser.contentPrincipal.spec !== + "resource://pdf.js/web/viewer.html" + ) { + return; + } + + lazy.PdfJsTelemetry.onGeckoview("save_as_pdf_tapped"); + + this.#callback = aCallback; + this.#browser.sendMessageToActor( + "PDFJS:Child:handleEvent", + { + type: "save", + }, + "GeckoViewPdfjs" + ); + } + + async save({ + blobUrl, + filename, + originalUrl, + options: { openInExternalApp }, + }) { + try { + const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate( + this.#browser + ); + + if (this.#callback) { + // "Save as PDF" from the share menu. + this.#callback.onSuccess({ + url: blobUrl, + filename, + originalUrl, + isPrivate, + }); + } else { + // "Download" or "Open in app" from the pdf.js toolbar. + this.#eventDispatcher.sendRequest({ + type: "GeckoView:SavePdf", + url: blobUrl, + filename, + originalUrl, + isPrivate, + skipConfirmation: true, + requestExternalApp: !!openInExternalApp, + }); + } + lazy.PdfJsTelemetry.onGeckoview("download_succeeded"); + } catch (e) { + lazy.PdfJsTelemetry.onGeckoview("download_failed"); + if (this.#callback) { + this.#callback?.onError(`Cannot save the pdf: ${e}.`); + } else { + warn`Cannot save the pdf because of: ${e}`; + } + } finally { + this.cleanup(); + } + } +} + +export class GeckoViewPdfjsParent extends GeckoViewActorParent { + #findHandler; + + #fileSaver; + + receiveMessage(aMsg) { + debug`receiveMessage: name=${aMsg.name}, data=${aMsg.data}`; + + switch (aMsg.name) { + case "PDFJS:Parent:updateControlState": + return this.#updateControlState(aMsg); + case "PDFJS:Parent:updateMatchesCount": + return this.#updateMatchesCount(aMsg); + case "PDFJS:Parent:addEventListener": + return this.#addEventListener(); + case "PDFJS:Parent:saveURL": + return this.#save(aMsg); + case "PDFJS:Parent:getNimbus": + return this.#getExperimentFeature(); + case "PDFJS:Parent:recordExposure": + return this.#recordExposure(); + case "PDFJS:Parent:reportTelemetry": + return this.#reportTelemetry(aMsg); + default: + break; + } + + return undefined; + } + + didDestroy() { + debug`didDestroy`; + + if (!this.#findHandler) { + return; + } + + // The eventDispatcher is null when the tab is destroyed. + if (this.eventDispatcher) { + this.eventDispatcher.unregisterListener(this.#findHandler, [ + "GeckoView:ClearMatches", + "GeckoView:DisplayMatches", + "GeckoView:FindInPage", + ]); + this.eventDispatcher.unregisterListener(this.#fileSaver, [ + "GeckoView:PDFSave", + ]); + } + + this.#findHandler.cleanup(); + this.#findHandler = null; + this.#fileSaver.cleanup(); + this.#fileSaver = null; + } + + #addEventListener() { + if (this.#findHandler) { + this.#findHandler.cleanup(); + return; + } + + this.#findHandler = new FindHandler(this.browser); + this.eventDispatcher.registerListener(this.#findHandler, [ + "GeckoView:ClearMatches", + "GeckoView:DisplayMatches", + "GeckoView:FindInPage", + ]); + + this.#fileSaver = new FileSaver(this.browser, this.eventDispatcher); + this.eventDispatcher.registerListener(this.#fileSaver, [ + "GeckoView:PDFSave", + ]); + } + + #updateMatchesCount({ data }) { + this.#findHandler.updateMatchesCount(data, Ci.nsITypeAheadFind.FIND_FOUND); + } + + #updateControlState({ data: { matchesCount, result } }) { + this.#findHandler.updateMatchesCount(matchesCount, result); + } + + #save({ data }) { + this.#fileSaver.save(data); + } + + async #getExperimentFeature() { + let result = null; + try { + const experimentActor = this.window.moduleManager.getActor( + "GeckoViewExperimentDelegate" + ); + result = await experimentActor.getExperimentFeature("pdfjs"); + } catch (e) { + warn`Cannot get experiment feature: ${e}`; + } + this.sendAsyncMessage("PDFJS:Child:getNimbus", result); + } + + async #recordExposure() { + try { + const experimentActor = this.window.moduleManager.getActor( + "GeckoViewExperimentDelegate" + ); + await experimentActor.recordExposure("pdfjs"); + } catch (e) { + warn`Cannot record experiment exposure: ${e}`; + } + } + + #reportTelemetry(aMsg) { + lazy.PdfJsTelemetry.report(aMsg.data); + } +} + +const { debug, warn } = GeckoViewPdfjsParent.initLogging( + "GeckoViewPdfjsParent" +); |