summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules/Messaging.jsm
blob: 30b7f5a96335a4e555d7fa614d3c56949d47025e (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
/* 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/. */
"use strict"

const { classes: Cc, interfaces: Ci, utils: Cu } = Components;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");

this.EXPORTED_SYMBOLS = ["sendMessageToJava", "Messaging"];

XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");

function sendMessageToJava(aMessage, aCallback) {
  Cu.reportError("sendMessageToJava is deprecated. Use Messaging API instead.");

  if (aCallback) {
    Messaging.sendRequestForResult(aMessage)
      .then(result => aCallback(result, null),
            error => aCallback(null, error));
  } else {
    Messaging.sendRequest(aMessage);
  }
}

var Messaging = {
  /**
   * Add a listener for the given message.
   *
   * Only one request listener can be registered for a given message.
   *
   * Example usage:
   *   // aData is data sent from Java with the request. The return value is
   *   // used to respond to the request. The return type *must* be an instance
   *   // of Object.
   *   let listener = function (aData) {
   *     if (aData == "foo") {
   *       return { response: "bar" };
   *     }
   *     return {};
   *   };
   *   Messaging.addListener(listener, "Demo:Request");
   *
   * The listener may also be a generator function, useful for performing a
   * task asynchronously. For example:
   *   let listener = function* (aData) {
   *     // Respond with "bar" after 2 seconds.
   *     yield new Promise(resolve => setTimeout(resolve, 2000));
   *     return { response: "bar" };
   *   };
   *   Messaging.addListener(listener, "Demo:Request");
   *
   * @param aListener Listener callback taking a single data parameter (see
   *                  example usage above).
   * @param aMessage  Event name that this listener should observe.
   */
  addListener: function (aListener, aMessage) {
    requestHandler.addListener(aListener, aMessage);
  },

  /**
   * Removes a listener for a given message.
   *
   * @param aMessage The event to stop listening for.
   */
  removeListener: function (aMessage) {
    requestHandler.removeListener(aMessage);
  },

  /**
   * Sends a request to Java.
   *
   * @param aMessage  Message to send; must be an object with a "type" property
   */
  sendRequest: function (aMessage) {
    Services.androidBridge.handleGeckoMessage(aMessage);
  },

  /**
   * Sends a request to Java, returning a Promise that resolves to the response.
   *
   * @param aMessage Message to send; must be an object with a "type" property
   * @returns A Promise resolving to the response
   */
  sendRequestForResult: function (aMessage) {
    return new Promise((resolve, reject) => {
      let id = uuidgen.generateUUID().toString();
      let obs = {
        observe: function (aSubject, aTopic, aData) {
          let data = JSON.parse(aData);
          if (data.__guid__ != id) {
            return;
          }

          Services.obs.removeObserver(obs, aMessage.type + ":Response");

          if (data.status === "success") {
            resolve(data.response);
          } else {
            reject(data.response);
          }
        }
      };

      aMessage.__guid__ = id;
      Services.obs.addObserver(obs, aMessage.type + ":Response", false);

      this.sendRequest(aMessage);
    });
  },

  /**
   * Handles a request from Java, using the given listener method.
   * This is mainly an internal method used by the RequestHandler object, but can be
   * used in nsIObserver.observe implmentations that fall outside the normal usage
   * patterns.
   *
   * @param aTopic    The string name of the message
   * @param aData     The data sent to the observe method from Java
   * @param aListener A function that takes a JSON data argument and returns a
   *                  response which is sent to Java.
   */
  handleRequest: Task.async(function* (aTopic, aData, aListener) {
    let wrapper = JSON.parse(aData);

    try {
      let response = yield aListener(wrapper.data);
      if (typeof response !== "object" || response === null) {
        throw new Error("Gecko request listener did not return an object");
      }

      Messaging.sendRequest({
        type: "Gecko:Request" + wrapper.id,
        response: response
      });
    } catch (e) {
      Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);

      Messaging.sendRequest({
        type: "Gecko:Request" + wrapper.id,
        error: {
          message: e.message || (e && e.toString()),
          stack: e.stack || Components.stack.formattedStack,
        }
      });
    }
  })
};

var requestHandler = {
  _listeners: {},

  addListener: function (aListener, aMessage) {
    if (aMessage in this._listeners) {
      throw new Error("Error in addListener: A listener already exists for message " + aMessage);
    }

    if (typeof aListener !== "function") {
      throw new Error("Error in addListener: Listener must be a function for message " + aMessage);
    }

    this._listeners[aMessage] = aListener;
    Services.obs.addObserver(this, aMessage, false);
  },

  removeListener: function (aMessage) {
    if (!(aMessage in this._listeners)) {
      throw new Error("Error in removeListener: There is no listener for message " + aMessage);
    }

    delete this._listeners[aMessage];
    Services.obs.removeObserver(this, aMessage);
  },

  observe: function(aSubject, aTopic, aData) {
    let listener = this._listeners[aTopic];
    Messaging.handleRequest(aTopic, aData, listener);
  }
};