Source code

Revision control

Copy as Markdown

Other Tools

/* 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() {
throw new Error("Cannot registerListener in child actor");
}
unregisterListener() {
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 nsIGeckoViewEventListener.
* @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 nsIGeckoViewEventListener.
* @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 nsIGeckoViewEventCallback.
* @param aFinalizer Optional finalizer implementing nsIGeckoViewEventFinalizer.
*/
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 nsIGeckoViewEventCallback.
*/
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.nsIGeckoViewView);
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);
}