| |
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 |
| |
5 package org.mozilla.gecko; |
| |
6 |
| |
7 import org.mozilla.gecko.GeckoAppShell; |
| |
8 import org.mozilla.gecko.GeckoEvent; |
| |
9 import org.mozilla.gecko.util.EventCallback; |
| |
10 import org.mozilla.gecko.util.GeckoEventListener; |
| |
11 import org.mozilla.gecko.util.NativeEventListener; |
| |
12 import org.mozilla.gecko.util.NativeJSContainer; |
| |
13 |
| |
14 import org.json.JSONException; |
| |
15 import org.json.JSONObject; |
| |
16 |
| |
17 import android.util.Log; |
| |
18 |
| |
19 import java.util.HashMap; |
| |
20 import java.util.List; |
| |
21 import java.util.Map; |
| |
22 import java.util.concurrent.CopyOnWriteArrayList; |
| |
23 |
| |
24 public final class EventDispatcher { |
| |
25 private static final String LOGTAG = "GeckoEventDispatcher"; |
| |
26 private static final String GUID = "__guid__"; |
| |
27 private static final String STATUS_CANCEL = "cancel"; |
| |
28 private static final String STATUS_ERROR = "error"; |
| |
29 private static final String STATUS_SUCCESS = "success"; |
| |
30 |
| |
31 /** |
| |
32 * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size |
| |
33 * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to |
| |
34 * empirically determine the initial capacity that avoids rehashing, we need to |
| |
35 * determine the initial size, divide it by 75%, and round up to the next power-of-2. |
| |
36 */ |
| |
37 private static final int GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap |
| |
38 private static final int GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured |
| |
39 |
| |
40 private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners = |
| |
41 new HashMap<String, List<NativeEventListener>>(GECKO_NATIVE_EVENTS_COUNT); |
| |
42 private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners = |
| |
43 new HashMap<String, List<GeckoEventListener>>(GECKO_JSON_EVENTS_COUNT); |
| |
44 |
| |
45 private <T> void registerListener(final Class<? extends List<T>> listType, |
| |
46 final Map<String, List<T>> listenersMap, |
| |
47 final T listener, |
| |
48 final String[] events) { |
| |
49 try { |
| |
50 synchronized (listenersMap) { |
| |
51 for (final String event : events) { |
| |
52 List<T> listeners = listenersMap.get(event); |
| |
53 if (listeners == null) { |
| |
54 listeners = listType.newInstance(); |
| |
55 listenersMap.put(event, listeners); |
| |
56 } |
| |
57 if (!AppConstants.RELEASE_BUILD && listeners.contains(listener)) { |
| |
58 throw new IllegalStateException("Already registered " + event); |
| |
59 } |
| |
60 listeners.add(listener); |
| |
61 } |
| |
62 } |
| |
63 } catch (final IllegalAccessException e) { |
| |
64 throw new IllegalArgumentException("Invalid new list type", e); |
| |
65 } catch (final InstantiationException e) { |
| |
66 throw new IllegalArgumentException("Invalid new list type", e); |
| |
67 } |
| |
68 } |
| |
69 |
| |
70 private <T> void checkNotRegistered(final Map<String, List<T>> listenersMap, |
| |
71 final String[] events) { |
| |
72 synchronized (listenersMap) { |
| |
73 for (final String event: events) { |
| |
74 if (listenersMap.get(event) != null) { |
| |
75 throw new IllegalStateException( |
| |
76 "Already registered " + event + " under a different type"); |
| |
77 } |
| |
78 } |
| |
79 } |
| |
80 } |
| |
81 |
| |
82 private <T> void unregisterListener(final Map<String, List<T>> listenersMap, |
| |
83 final T listener, |
| |
84 final String[] events) { |
| |
85 synchronized (listenersMap) { |
| |
86 for (final String event : events) { |
| |
87 List<T> listeners = listenersMap.get(event); |
| |
88 if ((listeners == null || |
| |
89 !listeners.remove(listener)) && !AppConstants.RELEASE_BUILD) { |
| |
90 throw new IllegalArgumentException(event + " was not registered"); |
| |
91 } |
| |
92 } |
| |
93 } |
| |
94 } |
| |
95 |
| |
96 @SuppressWarnings("unchecked") |
| |
97 public void registerGeckoThreadListener(final NativeEventListener listener, |
| |
98 final String... events) { |
| |
99 checkNotRegistered(mGeckoThreadJSONListeners, events); |
| |
100 |
| |
101 // For listeners running on the Gecko thread, we want to notify the listeners |
| |
102 // outside of our synchronized block, because the listeners may take an |
| |
103 // indeterminate amount of time to run. Therefore, to ensure concurrency when |
| |
104 // iterating the list outside of the synchronized block, we use a |
| |
105 // CopyOnWriteArrayList. |
| |
106 registerListener((Class)CopyOnWriteArrayList.class, |
| |
107 mGeckoThreadNativeListeners, listener, events); |
| |
108 } |
| |
109 |
| |
110 @Deprecated // Use NativeEventListener instead |
| |
111 @SuppressWarnings("unchecked") |
| |
112 private void registerGeckoThreadListener(final GeckoEventListener listener, |
| |
113 final String... events) { |
| |
114 checkNotRegistered(mGeckoThreadNativeListeners, events); |
| |
115 |
| |
116 registerListener((Class)CopyOnWriteArrayList.class, |
| |
117 mGeckoThreadJSONListeners, listener, events); |
| |
118 } |
| |
119 |
| |
120 public void unregisterGeckoThreadListener(final NativeEventListener listener, |
| |
121 final String... events) { |
| |
122 unregisterListener(mGeckoThreadNativeListeners, listener, events); |
| |
123 } |
| |
124 |
| |
125 @Deprecated // Use NativeEventListener instead |
| |
126 private void unregisterGeckoThreadListener(final GeckoEventListener listener, |
| |
127 final String... events) { |
| |
128 unregisterListener(mGeckoThreadJSONListeners, listener, events); |
| |
129 } |
| |
130 |
| |
131 @Deprecated // Use one of the variants above. |
| |
132 public void registerEventListener(final String event, final GeckoEventListener listener) { |
| |
133 registerGeckoThreadListener(listener, event); |
| |
134 } |
| |
135 |
| |
136 @Deprecated // Use one of the variants above |
| |
137 public void unregisterEventListener(final String event, final GeckoEventListener listener) { |
| |
138 unregisterGeckoThreadListener(listener, event); |
| |
139 } |
| |
140 |
| |
141 public void dispatchEvent(final NativeJSContainer message) { |
| |
142 EventCallback callback = null; |
| |
143 try { |
| |
144 // First try native listeners. |
| |
145 final String type = message.getString("type"); |
| |
146 |
| |
147 final List<NativeEventListener> listeners; |
| |
148 synchronized (mGeckoThreadNativeListeners) { |
| |
149 listeners = mGeckoThreadNativeListeners.get(type); |
| |
150 } |
| |
151 |
| |
152 final String guid = message.optString(GUID, null); |
| |
153 if (guid != null) { |
| |
154 callback = new GeckoEventCallback(guid, type); |
| |
155 } |
| |
156 |
| |
157 if (listeners != null) { |
| |
158 if (listeners.size() == 0) { |
| |
159 Log.w(LOGTAG, "No listeners for " + type); |
| |
160 } |
| |
161 for (final NativeEventListener listener : listeners) { |
| |
162 listener.handleMessage(type, message, callback); |
| |
163 } |
| |
164 // If we found native listeners, we assume we don't have any JSON listeners |
| |
165 // and return early. This assumption is checked when registering listeners. |
| |
166 return; |
| |
167 } |
| |
168 } catch (final IllegalArgumentException e) { |
| |
169 // Message doesn't have a "type" property, fallback to JSON |
| |
170 } |
| |
171 try { |
| |
172 // If we didn't find native listeners, try JSON listeners. |
| |
173 dispatchEvent(new JSONObject(message.toString()), callback); |
| |
174 } catch (final JSONException e) { |
| |
175 Log.e(LOGTAG, "Cannot parse JSON"); |
| |
176 } catch (final UnsupportedOperationException e) { |
| |
177 Log.e(LOGTAG, "Cannot convert message to JSON"); |
| |
178 } |
| |
179 } |
| |
180 |
| |
181 public void dispatchEvent(final JSONObject message, final EventCallback callback) { |
| |
182 // { |
| |
183 // "type": "value", |
| |
184 // "event_specific": "value", |
| |
185 // ... |
| |
186 try { |
| |
187 final String type = message.getString("type"); |
| |
188 |
| |
189 List<GeckoEventListener> listeners; |
| |
190 synchronized (mGeckoThreadJSONListeners) { |
| |
191 listeners = mGeckoThreadJSONListeners.get(type); |
| |
192 } |
| |
193 if (listeners == null || listeners.size() == 0) { |
| |
194 Log.w(LOGTAG, "No listeners for " + type); |
| |
195 |
| |
196 // If there are no listeners, cancel the callback to prevent Gecko-side observers |
| |
197 // from being leaked. |
| |
198 if (callback != null) { |
| |
199 callback.sendCancel(); |
| |
200 } |
| |
201 return; |
| |
202 } |
| |
203 for (final GeckoEventListener listener : listeners) { |
| |
204 listener.handleMessage(type, message); |
| |
205 } |
| |
206 } catch (final JSONException e) { |
| |
207 Log.e(LOGTAG, "handleGeckoMessage throws " + e, e); |
| |
208 } |
| |
209 } |
| |
210 |
| |
211 @Deprecated |
| |
212 public static void sendResponse(JSONObject message, Object response) { |
| |
213 sendResponseHelper(STATUS_SUCCESS, message, response); |
| |
214 } |
| |
215 |
| |
216 @Deprecated |
| |
217 public static void sendError(JSONObject message, Object response) { |
| |
218 sendResponseHelper(STATUS_ERROR, message, response); |
| |
219 } |
| |
220 |
| |
221 @Deprecated |
| |
222 private static void sendResponseHelper(String status, JSONObject message, Object response) { |
| |
223 try { |
| |
224 final JSONObject wrapper = new JSONObject(); |
| |
225 wrapper.put(GUID, message.getString(GUID)); |
| |
226 wrapper.put("status", status); |
| |
227 wrapper.put("response", response); |
| |
228 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent( |
| |
229 message.getString("type") + ":Response", wrapper.toString())); |
| |
230 } catch (final JSONException e) { |
| |
231 Log.e(LOGTAG, "Unable to send response", e); |
| |
232 } |
| |
233 } |
| |
234 |
| |
235 private static class GeckoEventCallback implements EventCallback { |
| |
236 private final String guid; |
| |
237 private final String type; |
| |
238 private boolean sent; |
| |
239 |
| |
240 public GeckoEventCallback(final String guid, final String type) { |
| |
241 this.guid = guid; |
| |
242 this.type = type; |
| |
243 } |
| |
244 |
| |
245 public void sendSuccess(final Object response) { |
| |
246 sendResponse(STATUS_SUCCESS, response); |
| |
247 } |
| |
248 |
| |
249 public void sendError(final Object response) { |
| |
250 sendResponse(STATUS_ERROR, response); |
| |
251 } |
| |
252 |
| |
253 public void sendCancel() { |
| |
254 sendResponse(STATUS_CANCEL, null); |
| |
255 } |
| |
256 |
| |
257 private void sendResponse(final String status, final Object response) { |
| |
258 if (sent) { |
| |
259 throw new IllegalStateException("Callback has already been executed for type=" + |
| |
260 type + ", guid=" + guid); |
| |
261 } |
| |
262 |
| |
263 sent = true; |
| |
264 |
| |
265 try { |
| |
266 final JSONObject wrapper = new JSONObject(); |
| |
267 wrapper.put(GUID, guid); |
| |
268 wrapper.put("status", status); |
| |
269 wrapper.put("response", response); |
| |
270 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(type + ":Response", |
| |
271 wrapper.toString())); |
| |
272 } catch (final JSONException e) { |
| |
273 Log.e(LOGTAG, "Unable to send response for: " + type, e); |
| |
274 } |
| |
275 } |
| |
276 } |
| |
277 } |