summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/geckoview/Messaging.sys.mjs
blob: e67161fedec9e5ff9ca87746f202a5793ccc8bd4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
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);
}