summaryrefslogtreecommitdiffstats
path: root/toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs')
-rw-r--r--toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs364
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"
+);