/* 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); }