diff options
Diffstat (limited to 'mobile/android/modules/geckoview/Messaging.sys.mjs')
-rw-r--r-- | mobile/android/modules/geckoview/Messaging.sys.mjs | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/Messaging.sys.mjs b/mobile/android/modules/geckoview/Messaging.sys.mjs new file mode 100644 index 0000000000..e67161fede --- /dev/null +++ b/mobile/android/modules/geckoview/Messaging.sys.mjs @@ -0,0 +1,319 @@ +/* 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/. */ + +const IS_PARENT_PROCESS = + Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT; + +class ChildActorDispatcher { + constructor(actor) { + this._actor = actor; + } + + // TODO: Bug 1658980 + registerListener(aListener, aEvents) { + throw new Error("Cannot registerListener in child actor"); + } + unregisterListener(aListener, aEvents) { + throw new Error("Cannot registerListener in child actor"); + } + + /** + * Sends a request to Java. + * + * @param aMsg Message to send; must be an object with a "type" property + */ + sendRequest(aMsg) { + this._actor.sendAsyncMessage("DispatcherMessage", aMsg); + } + + /** + * Sends a request to Java, returning a Promise that resolves to the response. + * + * @param aMsg Message to send; must be an object with a "type" property + * @return A Promise resolving to the response + */ + sendRequestForResult(aMsg) { + return this._actor.sendQuery("DispatcherQuery", aMsg); + } +} + +function DispatcherDelegate(aDispatcher, aMessageManager) { + this._dispatcher = aDispatcher; + this._messageManager = aMessageManager; + + if (!aDispatcher) { + // Child process. + // TODO: this doesn't work with Fission, remove this code path once every + // consumer has been migrated. Bug 1569360. + this._replies = new Map(); + (aMessageManager || Services.cpmm).addMessageListener( + "GeckoView:MessagingReply", + this + ); + } +} + +DispatcherDelegate.prototype = { + /** + * Register a listener to be notified of event(s). + * + * @param aListener Target listener implementing nsIAndroidEventListener. + * @param aEvents String or array of strings of events to listen to. + */ + registerListener(aListener, aEvents) { + if (!this._dispatcher) { + throw new Error("Can only listen in parent process"); + } + this._dispatcher.registerListener(aListener, aEvents); + }, + + /** + * Unregister a previously-registered listener. + * + * @param aListener Registered listener implementing nsIAndroidEventListener. + * @param aEvents String or array of strings of events to stop listening to. + */ + unregisterListener(aListener, aEvents) { + if (!this._dispatcher) { + throw new Error("Can only listen in parent process"); + } + this._dispatcher.unregisterListener(aListener, aEvents); + }, + + /** + * Dispatch an event to registered listeners for that event, and pass an + * optional data object and/or a optional callback interface to the + * listeners. + * + * @param aEvent Name of event to dispatch. + * @param aData Optional object containing data for the event. + * @param aCallback Optional callback implementing nsIAndroidEventCallback. + * @param aFinalizer Optional finalizer implementing nsIAndroidEventFinalizer. + */ + dispatch(aEvent, aData, aCallback, aFinalizer) { + if (this._dispatcher) { + this._dispatcher.dispatch(aEvent, aData, aCallback, aFinalizer); + return; + } + + const mm = this._messageManager || Services.cpmm; + const forwardData = { + global: !this._messageManager, + event: aEvent, + data: aData, + }; + + if (aCallback) { + const uuid = Services.uuid.generateUUID().toString(); + this._replies.set(uuid, { + callback: aCallback, + finalizer: aFinalizer, + }); + forwardData.uuid = uuid; + } + + mm.sendAsyncMessage("GeckoView:Messaging", forwardData); + }, + + /** + * Sends a request to Java. + * + * @param aMsg Message to send; must be an object with a "type" property + * @param aCallback Optional callback implementing nsIAndroidEventCallback. + */ + sendRequest(aMsg, aCallback) { + const type = aMsg.type; + aMsg.type = undefined; + this.dispatch(type, aMsg, aCallback); + }, + + /** + * Sends a request to Java, returning a Promise that resolves to the response. + * + * @param aMsg Message to send; must be an object with a "type" property + * @return A Promise resolving to the response + */ + sendRequestForResult(aMsg) { + return new Promise((resolve, reject) => { + const type = aMsg.type; + aMsg.type = undefined; + + // Manually release the resolve/reject functions after one callback is + // received, so the JS GC is not tied up with the Java GC. + const onCallback = (callback, ...args) => { + if (callback) { + callback(...args); + } + resolve = undefined; + reject = undefined; + }; + const callback = { + onSuccess: result => onCallback(resolve, result), + onError: error => onCallback(reject, error), + onFinalize: _ => onCallback(reject), + }; + this.dispatch(type, aMsg, callback, callback); + }); + }, + + finalize() { + if (!this._replies) { + return; + } + this._replies.forEach(reply => { + if (typeof reply.finalizer === "function") { + reply.finalizer(); + } else if (reply.finalizer) { + reply.finalizer.onFinalize(); + } + }); + this._replies.clear(); + }, + + receiveMessage(aMsg) { + const { uuid, type } = aMsg.data; + const reply = this._replies.get(uuid); + if (!reply) { + return; + } + + if (type === "success") { + reply.callback.onSuccess(aMsg.data.response); + } else if (type === "error") { + reply.callback.onError(aMsg.data.response); + } else if (type === "finalize") { + if (typeof reply.finalizer === "function") { + reply.finalizer(); + } else if (reply.finalizer) { + reply.finalizer.onFinalize(); + } + this._replies.delete(uuid); + } else { + throw new Error("invalid reply type"); + } + }, +}; + +export var EventDispatcher = { + instance: new DispatcherDelegate( + IS_PARENT_PROCESS ? Services.androidBridge : undefined + ), + + /** + * Return an EventDispatcher instance for a chrome DOM window. In a content + * process, return a proxy through the message manager that automatically + * forwards events to the main process. + * + * To force using a message manager proxy (for example in a frame script + * environment), call forMessageManager. + * + * @param aWindow a chrome DOM window. + */ + for(aWindow) { + const view = + aWindow && + aWindow.arguments && + aWindow.arguments[0] && + aWindow.arguments[0].QueryInterface(Ci.nsIAndroidView); + + if (!view) { + const mm = !IS_PARENT_PROCESS && aWindow && aWindow.messageManager; + if (!mm) { + throw new Error( + "window is not a GeckoView-connected window and does" + + " not have a message manager" + ); + } + return this.forMessageManager(mm); + } + + return new DispatcherDelegate(view); + }, + + /** + * Returns a named EventDispatcher, which can communicate with the + * corresponding EventDispatcher on the java side. + */ + byName(aName) { + if (!IS_PARENT_PROCESS) { + return undefined; + } + const dispatcher = Services.androidBridge.getDispatcherByName(aName); + return new DispatcherDelegate(dispatcher); + }, + + /** + * Return an EventDispatcher instance for a message manager associated with a + * window. + * + * @param aWindow a message manager. + */ + forMessageManager(aMessageManager) { + return new DispatcherDelegate(null, aMessageManager); + }, + + /** + * Return the EventDispatcher instance associated with an actor. + * + * @param aActor an actor + */ + forActor(aActor) { + return new ChildActorDispatcher(aActor); + }, + + receiveMessage(aMsg) { + // aMsg.data includes keys: global, event, data, uuid + let callback; + if (aMsg.data.uuid) { + const reply = (type, response) => { + const mm = aMsg.data.global ? aMsg.target : aMsg.target.messageManager; + if (!mm) { + if (type === "finalize") { + // It's normal for the finalize call to come after the browser has + // been destroyed. We can gracefully handle that case despite + // having no message manager. + return; + } + throw Error( + `No message manager for ${aMsg.data.event}:${type} reply` + ); + } + mm.sendAsyncMessage("GeckoView:MessagingReply", { + type, + response, + uuid: aMsg.data.uuid, + }); + }; + callback = { + onSuccess: response => reply("success", response), + onError: error => reply("error", error), + onFinalize: () => reply("finalize"), + }; + } + + try { + if (aMsg.data.global) { + this.instance.dispatch( + aMsg.data.event, + aMsg.data.data, + callback, + callback + ); + return; + } + + const win = aMsg.target.ownerGlobal; + const dispatcher = win.WindowEventDispatcher || this.for(win); + dispatcher.dispatch(aMsg.data.event, aMsg.data.data, callback, callback); + } catch (e) { + callback?.onError(`Error getting dispatcher: ${e}`); + throw e; + } + }, +}; + +if (IS_PARENT_PROCESS) { + Services.mm.addMessageListener("GeckoView:Messaging", EventDispatcher); + Services.ppmm.addMessageListener("GeckoView:Messaging", EventDispatcher); +} |