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