1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4"use strict"; 5 6const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 7const { XPCOMUtils } = ChromeUtils.import( 8 "resource://gre/modules/XPCOMUtils.jsm" 9); 10 11var EXPORTED_SYMBOLS = ["EventDispatcher"]; 12 13const IS_PARENT_PROCESS = 14 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT; 15 16function DispatcherDelegate(aDispatcher, aMessageManager) { 17 this._dispatcher = aDispatcher; 18 this._messageManager = aMessageManager; 19 20 if (!aDispatcher) { 21 // Child process. 22 this._replies = new Map(); 23 (aMessageManager || Services.cpmm).addMessageListener( 24 "GeckoView:MessagingReply", 25 this 26 ); 27 } 28} 29 30DispatcherDelegate.prototype = { 31 /** 32 * Register a listener to be notified of event(s). 33 * 34 * @param aListener Target listener implementing nsIAndroidEventListener. 35 * @param aEvents String or array of strings of events to listen to. 36 */ 37 registerListener(aListener, aEvents) { 38 if (!this._dispatcher) { 39 throw new Error("Can only listen in parent process"); 40 } 41 this._dispatcher.registerListener(aListener, aEvents); 42 }, 43 44 /** 45 * Unregister a previously-registered listener. 46 * 47 * @param aListener Registered listener implementing nsIAndroidEventListener. 48 * @param aEvents String or array of strings of events to stop listening to. 49 */ 50 unregisterListener(aListener, aEvents) { 51 if (!this._dispatcher) { 52 throw new Error("Can only listen in parent process"); 53 } 54 this._dispatcher.unregisterListener(aListener, aEvents); 55 }, 56 57 /** 58 * Dispatch an event to registered listeners for that event, and pass an 59 * optional data object and/or a optional callback interface to the 60 * listeners. 61 * 62 * @param aEvent Name of event to dispatch. 63 * @param aData Optional object containing data for the event. 64 * @param aCallback Optional callback implementing nsIAndroidEventCallback. 65 * @param aFinalizer Optional finalizer implementing nsIAndroidEventFinalizer. 66 */ 67 dispatch(aEvent, aData, aCallback, aFinalizer) { 68 if (this._dispatcher) { 69 this._dispatcher.dispatch(aEvent, aData, aCallback, aFinalizer); 70 return; 71 } 72 73 const mm = this._messageManager || Services.cpmm; 74 const forwardData = { 75 global: !this._messageManager, 76 event: aEvent, 77 data: aData, 78 }; 79 80 if (aCallback) { 81 const uuid = Services.uuid.generateUUID().toString(); 82 this._replies.set(uuid, { 83 callback: aCallback, 84 finalizer: aFinalizer, 85 }); 86 forwardData.uuid = uuid; 87 } 88 89 mm.sendAsyncMessage("GeckoView:Messaging", forwardData); 90 }, 91 92 /** 93 * Sends a request to Java. 94 * 95 * @param aMsg Message to send; must be an object with a "type" property 96 * @param aCallback Optional callback implementing nsIAndroidEventCallback. 97 */ 98 sendRequest(aMsg, aCallback) { 99 const type = aMsg.type; 100 aMsg.type = undefined; 101 this.dispatch(type, aMsg, aCallback); 102 }, 103 104 /** 105 * Sends a request to Java, returning a Promise that resolves to the response. 106 * 107 * @param aMsg Message to send; must be an object with a "type" property 108 * @return A Promise resolving to the response 109 */ 110 sendRequestForResult(aMsg) { 111 return new Promise((resolve, reject) => { 112 const type = aMsg.type; 113 aMsg.type = undefined; 114 115 // Manually release the resolve/reject functions after one callback is 116 // received, so the JS GC is not tied up with the Java GC. 117 const onCallback = (callback, ...args) => { 118 if (callback) { 119 callback(...args); 120 } 121 resolve = undefined; 122 reject = undefined; 123 }; 124 const callback = { 125 onSuccess: result => onCallback(resolve, result), 126 onError: error => onCallback(reject, error), 127 onFinalize: _ => onCallback(reject), 128 }; 129 this.dispatch(type, aMsg, callback, callback); 130 }); 131 }, 132 133 finalize() { 134 if (!this._replies) { 135 return; 136 } 137 this._replies.forEach(reply => { 138 if (typeof reply.finalizer === "function") { 139 reply.finalizer(); 140 } else if (reply.finalizer) { 141 reply.finalizer.onFinalize(); 142 } 143 }); 144 this._replies.clear(); 145 }, 146 147 receiveMessage(aMsg) { 148 const { uuid, type } = aMsg.data; 149 const reply = this._replies.get(uuid); 150 if (!reply) { 151 return; 152 } 153 154 if (type === "success") { 155 reply.callback.onSuccess(aMsg.data.response); 156 } else if (type === "error") { 157 reply.callback.onError(aMsg.data.response); 158 } else if (type === "finalize") { 159 if (typeof reply.finalizer === "function") { 160 reply.finalizer(); 161 } else if (reply.finalizer) { 162 reply.finalizer.onFinalize(); 163 } 164 this._replies.delete(uuid); 165 } else { 166 throw new Error("invalid reply type"); 167 } 168 }, 169}; 170 171var EventDispatcher = { 172 instance: new DispatcherDelegate( 173 IS_PARENT_PROCESS ? Services.androidBridge : undefined 174 ), 175 176 /** 177 * Return an EventDispatcher instance for a chrome DOM window. In a content 178 * process, return a proxy through the message manager that automatically 179 * forwards events to the main process. 180 * 181 * To force using a message manager proxy (for example in a frame script 182 * environment), call forMessageManager. 183 * 184 * @param aWindow a chrome DOM window. 185 */ 186 for(aWindow) { 187 const view = 188 aWindow && 189 aWindow.arguments && 190 aWindow.arguments[0] && 191 aWindow.arguments[0].QueryInterface(Ci.nsIAndroidView); 192 193 if (!view) { 194 const mm = !IS_PARENT_PROCESS && aWindow && aWindow.messageManager; 195 if (!mm) { 196 throw new Error( 197 "window is not a GeckoView-connected window and does" + 198 " not have a message manager" 199 ); 200 } 201 return this.forMessageManager(mm); 202 } 203 204 return new DispatcherDelegate(view); 205 }, 206 207 /** 208 * Returns a named EventDispatcher, which can communicate with the 209 * corresponding EventDispatcher on the java side. 210 */ 211 byName(aName) { 212 if (!IS_PARENT_PROCESS) { 213 return undefined; 214 } 215 const dispatcher = Services.androidBridge.getDispatcherByName(aName); 216 return new DispatcherDelegate(dispatcher); 217 }, 218 219 /** 220 * Return an EventDispatcher instance for a message manager associated with a 221 * window. 222 * 223 * @param aWindow a message manager. 224 */ 225 forMessageManager(aMessageManager) { 226 return new DispatcherDelegate(null, aMessageManager); 227 }, 228 229 receiveMessage(aMsg) { 230 // aMsg.data includes keys: global, event, data, uuid 231 let callback; 232 if (aMsg.data.uuid) { 233 const reply = (type, response) => { 234 const mm = aMsg.data.global ? aMsg.target : aMsg.target.messageManager; 235 if (!mm) { 236 if (type === "finalize") { 237 // It's normal for the finalize call to come after the browser has 238 // been destroyed. We can gracefully handle that case despite 239 // having no message manager. 240 return; 241 } 242 throw Error( 243 `No message manager for ${aMsg.data.event}:${type} reply` 244 ); 245 } 246 mm.sendAsyncMessage("GeckoView:MessagingReply", { 247 type, 248 response, 249 uuid: aMsg.data.uuid, 250 }); 251 }; 252 callback = { 253 onSuccess: response => reply("success", response), 254 onError: error => reply("error", error), 255 onFinalize: () => reply("finalize"), 256 }; 257 } 258 259 try { 260 if (aMsg.data.global) { 261 this.instance.dispatch( 262 aMsg.data.event, 263 aMsg.data.data, 264 callback, 265 callback 266 ); 267 return; 268 } 269 270 const win = aMsg.target.ownerGlobal; 271 const dispatcher = win.WindowEventDispatcher || this.for(win); 272 dispatcher.dispatch(aMsg.data.event, aMsg.data.data, callback, callback); 273 } catch (e) { 274 callback?.onError(`Error getting dispatcher: ${e}`); 275 throw e; 276 } 277 }, 278}; 279 280if (IS_PARENT_PROCESS) { 281 Services.mm.addMessageListener("GeckoView:Messaging", EventDispatcher); 282 Services.ppmm.addMessageListener("GeckoView:Messaging", EventDispatcher); 283} 284