1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2  * vim: ts=4 sw=4 expandtab:
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 package org.mozilla.gecko;
8 
9 import org.mozilla.gecko.annotation.ReflectionTarget;
10 import org.mozilla.gecko.annotation.RobocopTarget;
11 import org.mozilla.gecko.annotation.WrapForJNI;
12 import org.mozilla.gecko.mozglue.JNIObject;
13 import org.mozilla.gecko.util.BundleEventListener;
14 import org.mozilla.gecko.util.EventCallback;
15 import org.mozilla.gecko.util.GeckoBundle;
16 import org.mozilla.gecko.util.ThreadUtils;
17 import org.mozilla.geckoview.BuildConfig;
18 
19 import android.os.Handler;
20 import android.support.annotation.AnyThread;
21 import android.util.Log;
22 
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 
28 @RobocopTarget
29 public final class EventDispatcher extends JNIObject {
30     private static final String LOGTAG = "GeckoEventDispatcher";
31 
32     private static final EventDispatcher INSTANCE = new EventDispatcher();
33 
34     /**
35      * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
36      * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
37      * empirically determine the initial capacity that avoids rehashing, we need to
38      * determine the initial size, divide it by 75%, and round up to the next power-of-2.
39      */
40     private static final int DEFAULT_GECKO_EVENTS_COUNT = 64; // Empirically measured
41     private static final int DEFAULT_UI_EVENTS_COUNT = 128; // Empirically measured
42     private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 64; // Empirically measured
43 
44     // GeckoBundle-based events.
45     private final MultiMap<String, BundleEventListener> mGeckoThreadListeners =
46         new MultiMap<>(DEFAULT_GECKO_EVENTS_COUNT);
47     private final MultiMap<String, BundleEventListener> mUiThreadListeners =
48         new MultiMap<>(DEFAULT_UI_EVENTS_COUNT);
49     private final MultiMap<String, BundleEventListener> mBackgroundThreadListeners =
50         new MultiMap<>(DEFAULT_BACKGROUND_EVENTS_COUNT);
51 
52     private boolean mAttachedToGecko;
53     private final NativeQueue mNativeQueue;
54 
55     @ReflectionTarget
56     @WrapForJNI(calledFrom = "gecko")
getInstance()57     public static EventDispatcher getInstance() {
58         return INSTANCE;
59     }
60 
EventDispatcher()61     /* package */ EventDispatcher() {
62         mNativeQueue = GeckoThread.getNativeQueue();
63     }
64 
EventDispatcher(final NativeQueue queue)65     public EventDispatcher(final NativeQueue queue) {
66         mNativeQueue = queue;
67     }
68 
isReadyForDispatchingToGecko()69     private boolean isReadyForDispatchingToGecko() {
70         return mNativeQueue.isReady();
71     }
72 
73     @WrapForJNI @Override // JNIObject
disposeNative()74     protected native void disposeNative();
75 
76     @WrapForJNI private static final int DETACHED = 0;
77     @WrapForJNI private static final int ATTACHED = 1;
78     @WrapForJNI private static final int REATTACHING = 2;
79 
80     @WrapForJNI(calledFrom = "gecko")
setAttachedToGecko(final int state)81     private synchronized void setAttachedToGecko(final int state) {
82         if (mAttachedToGecko && state == DETACHED) {
83             dispose(false);
84         }
85         mAttachedToGecko = (state == ATTACHED);
86     }
87 
dispose(final boolean force)88     private void dispose(final boolean force) {
89         final Handler geckoHandler = ThreadUtils.sGeckoHandler;
90         if (geckoHandler == null) {
91             return;
92         }
93 
94         geckoHandler.post(new Runnable() {
95             @Override
96             public void run() {
97                 if (force || !mAttachedToGecko) {
98                     disposeNative();
99                 }
100             }
101         });
102     }
103 
registerListener(final Class<?> listType, final MultiMap<String, T> listenersMap, final T listener, final String[] events)104     private <T> void registerListener(final Class<?> listType,
105                                       final MultiMap<String, T> listenersMap,
106                                       final T listener,
107                                       final String[] events) {
108         try {
109             synchronized (listenersMap) {
110                 for (final String event : events) {
111                     if (event == null) {
112                         continue;
113                     }
114                     if (!BuildConfig.RELEASE_OR_BETA && listenersMap.containsEntry(event, listener)) {
115                         throw new IllegalStateException("Already registered " + event);
116                     }
117                     listenersMap.add(event, listener);
118                 }
119             }
120         } catch (final Exception e) {
121             throw new IllegalArgumentException("Invalid new list type", e);
122         }
123     }
124 
checkNotRegisteredElsewhere(final MultiMap<String, ?> allowedMap, final String[] events)125     private void checkNotRegisteredElsewhere(final MultiMap<String, ?> allowedMap,
126                                              final String[] events) {
127         if (BuildConfig.RELEASE_OR_BETA) {
128             // for performance reasons, we only check for
129             // already-registered listeners in non-release builds.
130             return;
131         }
132         for (final MultiMap<String, ?> listenersMap : Arrays.asList(mGeckoThreadListeners,
133                                                                mUiThreadListeners,
134                                                                mBackgroundThreadListeners)) {
135             if (listenersMap == allowedMap) {
136                 continue;
137             }
138             synchronized (listenersMap) {
139                 for (final String event : events) {
140                     if (listenersMap.containsKey(event)) {
141                         throw new IllegalStateException(
142                             "Already registered " + event + " under a different type");
143                     }
144                 }
145             }
146         }
147     }
148 
unregisterListener(final MultiMap<String, T> listenersMap, final T listener, final String[] events)149     private <T> void unregisterListener(final MultiMap<String, T> listenersMap,
150                                         final T listener,
151                                         final String[] events) {
152         synchronized (listenersMap) {
153             for (final String event : events) {
154                 if (event == null) {
155                     continue;
156                 }
157 
158                 if (!listenersMap.remove(event, listener) && !BuildConfig.RELEASE_OR_BETA) {
159                     throw new IllegalArgumentException(event + " was not registered");
160                 }
161             }
162         }
163     }
164 
registerGeckoThreadListener(final BundleEventListener listener, final String... events)165     public void registerGeckoThreadListener(final BundleEventListener listener,
166                                             final String... events) {
167         checkNotRegisteredElsewhere(mGeckoThreadListeners, events);
168 
169         // For listeners running on the Gecko thread, we want to notify the listeners
170         // outside of our synchronized block, because the listeners may take an
171         // indeterminate amount of time to run. Therefore, to ensure concurrency when
172         // iterating the list outside of the synchronized block, we use a
173         // CopyOnWriteArrayList.
174         registerListener(CopyOnWriteArrayList.class,
175                          mGeckoThreadListeners, listener, events);
176     }
177 
registerUiThreadListener(final BundleEventListener listener, final String... events)178     public void registerUiThreadListener(final BundleEventListener listener,
179                                          final String... events) {
180         checkNotRegisteredElsewhere(mUiThreadListeners, events);
181 
182         registerListener(ArrayList.class,
183                          mUiThreadListeners, listener, events);
184     }
185 
186     @ReflectionTarget
registerBackgroundThreadListener(final BundleEventListener listener, final String... events)187     public void registerBackgroundThreadListener(final BundleEventListener listener,
188                                                  final String... events) {
189         checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);
190 
191         registerListener(ArrayList.class,
192                          mBackgroundThreadListeners, listener, events);
193     }
194 
unregisterGeckoThreadListener(final BundleEventListener listener, final String... events)195     public void unregisterGeckoThreadListener(final BundleEventListener listener,
196                                               final String... events) {
197         unregisterListener(mGeckoThreadListeners, listener, events);
198     }
199 
unregisterUiThreadListener(final BundleEventListener listener, final String... events)200     public void unregisterUiThreadListener(final BundleEventListener listener,
201                                            final String... events) {
202         unregisterListener(mUiThreadListeners, listener, events);
203     }
204 
unregisterBackgroundThreadListener(final BundleEventListener listener, final String... events)205     public void unregisterBackgroundThreadListener(final BundleEventListener listener,
206                                                    final String... events) {
207         unregisterListener(mBackgroundThreadListeners, listener, events);
208     }
209 
210     @WrapForJNI
hasGeckoListener(final String event)211     private native boolean hasGeckoListener(final String event);
212 
213     @WrapForJNI(dispatchTo = "gecko")
dispatchToGecko(final String event, final GeckoBundle data, final EventCallback callback)214     private native void dispatchToGecko(final String event, final GeckoBundle data,
215                                         final EventCallback callback);
216 
217     /**
218      * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
219      *
220      * @param type Event type
221      * @param message Bundle message
222      */
dispatch(final String type, final GeckoBundle message)223     public void dispatch(final String type, final GeckoBundle message) {
224         dispatch(type, message, /* callback */ null);
225     }
226 
227     /**
228      * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
229      *
230      * @param type Event type
231      * @param message Bundle message
232      * @param callback Optional object for callbacks from events.
233      */
234     @AnyThread
dispatch(final String type, final GeckoBundle message, final EventCallback callback)235     public void dispatch(final String type, final GeckoBundle message,
236                          final EventCallback callback) {
237         final boolean isGeckoReady;
238         synchronized (this) {
239             isGeckoReady = isReadyForDispatchingToGecko();
240             if (isGeckoReady && mAttachedToGecko && hasGeckoListener(type)) {
241                 dispatchToGecko(type, message, JavaCallbackDelegate.wrap(callback));
242                 return;
243             }
244         }
245 
246         dispatchToThreads(type, message, callback, isGeckoReady);
247     }
248 
249     @WrapForJNI(calledFrom = "gecko")
dispatchToThreads(final String type, final GeckoBundle message, final EventCallback callback)250     private boolean dispatchToThreads(final String type,
251                                       final GeckoBundle message,
252                                       final EventCallback callback) {
253         return dispatchToThreads(type, message, callback, /* isGeckoReady */ true);
254     }
255 
dispatchToThreads(final String type, final GeckoBundle message, final EventCallback callback, final boolean isGeckoReady)256     private boolean dispatchToThreads(final String type,
257                                       final GeckoBundle message,
258                                       final EventCallback callback,
259                                       final boolean isGeckoReady) {
260         final List<BundleEventListener> geckoListeners;
261         synchronized (mGeckoThreadListeners) {
262             geckoListeners = mGeckoThreadListeners.get(type);
263         }
264         if (!geckoListeners.isEmpty()) {
265             final boolean onGeckoThread = ThreadUtils.isOnGeckoThread();
266             final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
267 
268             for (final BundleEventListener listener : geckoListeners) {
269                 // For other threads, we always dispatch asynchronously. However, for
270                 // Gecko listeners only, we dispatch synchronously if we're already on
271                 // Gecko thread.
272                 if (onGeckoThread) {
273                     listener.handleMessage(type, message, wrappedCallback);
274                     continue;
275                 }
276                 ThreadUtils.sGeckoHandler.post(new Runnable() {
277                     @Override
278                     public void run() {
279                         listener.handleMessage(type, message, wrappedCallback);
280                     }
281                 });
282             }
283             return true;
284         }
285 
286         if (dispatchToThread(type, message, callback,
287                              mUiThreadListeners, ThreadUtils.getUiHandler())) {
288             return true;
289         }
290 
291         if (dispatchToThread(type, message, callback,
292                              mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) {
293             return true;
294         }
295 
296         if (!isGeckoReady) {
297             // Usually, we discard an event if there is no listeners for it by
298             // the time of the dispatch. However, if Gecko(View) is not ready and
299             // there is no listener for this event that's possibly headed to
300             // Gecko, we make a special exception to queue this event until
301             // Gecko(View) is ready. This way, Gecko can first register its
302             // listeners, and accept the event when it is ready.
303             mNativeQueue.queueUntilReady(this, "dispatchToGecko",
304                 String.class, type,
305                 GeckoBundle.class, message,
306                 EventCallback.class, JavaCallbackDelegate.wrap(callback));
307             return true;
308         }
309 
310         final String error = "No listener for " + type;
311         if (callback != null) {
312             callback.sendError(error);
313         }
314 
315         Log.w(LOGTAG, error);
316         return false;
317     }
318 
319     @WrapForJNI
hasListener(final String event)320     public boolean hasListener(final String event) {
321         for (final MultiMap<String, ?> listenersMap : Arrays.asList(mGeckoThreadListeners,
322                 mUiThreadListeners,
323                 mBackgroundThreadListeners)) {
324             synchronized (listenersMap) {
325                 if (listenersMap.containsKey(event)) {
326                     return true;
327                 }
328             }
329         }
330 
331         return false;
332     }
333 
dispatchToThread(final String type, final GeckoBundle message, final EventCallback callback, final MultiMap<String, BundleEventListener> listenersMap, final Handler thread)334     private boolean dispatchToThread(final String type,
335                                      final GeckoBundle message,
336                                      final EventCallback callback,
337                                      final MultiMap<String, BundleEventListener> listenersMap,
338                                      final Handler thread) {
339         // We need to hold the lock throughout dispatching, to ensure the listeners list
340         // is consistent, while we iterate over it. We don't have to worry about listeners
341         // running for a long time while we have the lock, because the listeners will run
342         // on a separate thread.
343         synchronized (listenersMap) {
344             if (!listenersMap.containsKey(type)) {
345                 return false;
346             }
347 
348             // Use a delegate to make sure callbacks happen on a specific thread.
349             final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
350 
351             // Event listeners will call | callback.sendError | if applicable.
352             for (final BundleEventListener listener : listenersMap.get(type)) {
353                 thread.post(new Runnable() {
354                     @Override
355                     public void run() {
356                         listener.handleMessage(type, message, wrappedCallback);
357                     }
358                 });
359             }
360             return true;
361         }
362     }
363 
364     @Override
finalize()365     protected void finalize() throws Throwable {
366         dispose(true);
367     }
368 
369     private static class NativeCallbackDelegate extends JNIObject implements EventCallback {
370         @WrapForJNI(calledFrom = "gecko")
NativeCallbackDelegate()371         private NativeCallbackDelegate() {
372         }
373 
374         @Override // JNIObject
disposeNative()375         protected void disposeNative() {
376             // We dispose in finalize().
377             throw new UnsupportedOperationException();
378         }
379 
380         @WrapForJNI(dispatchTo = "proxy") @Override // EventCallback
sendSuccess(Object response)381         public native void sendSuccess(Object response);
382 
383         @WrapForJNI(dispatchTo = "proxy") @Override // EventCallback
sendError(Object response)384         public native void sendError(Object response);
385 
386         @WrapForJNI(dispatchTo = "gecko") @Override // Object
finalize()387         protected native void finalize();
388     }
389 
390     private static class JavaCallbackDelegate implements EventCallback {
391         private final Thread mOriginalThread = Thread.currentThread();
392         private final EventCallback mCallback;
393 
wrap(final EventCallback callback)394         public static EventCallback wrap(final EventCallback callback) {
395             if (callback == null) {
396                 return null;
397             }
398             if (callback instanceof NativeCallbackDelegate) {
399                 // NativeCallbackDelegate always posts to Gecko thread if needed.
400                 return callback;
401             }
402             return new JavaCallbackDelegate(callback);
403         }
404 
JavaCallbackDelegate(final EventCallback callback)405         JavaCallbackDelegate(final EventCallback callback) {
406             mCallback = callback;
407         }
408 
makeCallback(final boolean callSuccess, final Object rawResponse)409         private void makeCallback(final boolean callSuccess, final Object rawResponse) {
410             final Object response;
411             if (rawResponse instanceof Number) {
412                 // There is ambiguity because a number can be converted to either int or
413                 // double, so e.g. the user can be expecting a double when we give it an
414                 // int. To avoid these pitfalls, we disallow all numbers. The workaround
415                 // is to wrap the number in a JS object / GeckoBundle, which supports
416                 // type coersion for numbers.
417                 throw new UnsupportedOperationException(
418                         "Cannot use number as Java callback result");
419             } else if (rawResponse != null && rawResponse.getClass().isArray()) {
420                 // Same with arrays.
421                 throw new UnsupportedOperationException(
422                         "Cannot use arrays as Java callback result");
423             } else if (rawResponse instanceof Character) {
424                 response = rawResponse.toString();
425             } else {
426                 response = rawResponse;
427             }
428 
429             // Call back synchronously if we happen to be on the same thread as the thread
430             // making the original request.
431             if (ThreadUtils.isOnThread(mOriginalThread)) {
432                 if (callSuccess) {
433                     mCallback.sendSuccess(response);
434                 } else {
435                     mCallback.sendError(response);
436                 }
437                 return;
438             }
439 
440             // Make callback on the thread of the original request, if the original thread
441             // is the UI or Gecko thread. Otherwise default to the background thread.
442             final Handler handler =
443                     mOriginalThread == ThreadUtils.getUiThread() ? ThreadUtils.getUiHandler() :
444                     mOriginalThread == ThreadUtils.sGeckoThread ? ThreadUtils.sGeckoHandler :
445                                                                  ThreadUtils.getBackgroundHandler();
446             final EventCallback callback = mCallback;
447 
448             handler.post(new Runnable() {
449                 @Override
450                 public void run() {
451                     if (callSuccess) {
452                         callback.sendSuccess(response);
453                     } else {
454                         callback.sendError(response);
455                     }
456                 }
457             });
458         }
459 
460         @Override // EventCallback
sendSuccess(final Object response)461         public void sendSuccess(final Object response) {
462             makeCallback(/* success */ true, response);
463         }
464 
465         @Override // EventCallback
sendError(final Object response)466         public void sendError(final Object response) {
467             makeCallback(/* success */ false, response);
468         }
469     }
470 }
471