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