EventDispatcher.java (18927B)
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 android.os.Handler; 10 import android.util.Log; 11 import androidx.annotation.AnyThread; 12 import java.util.ArrayDeque; 13 import java.util.Arrays; 14 import java.util.Deque; 15 import java.util.HashMap; 16 import java.util.HashSet; 17 import java.util.Map; 18 import java.util.Set; 19 import org.mozilla.gecko.annotation.ReflectionTarget; 20 import org.mozilla.gecko.annotation.RobocopTarget; 21 import org.mozilla.gecko.annotation.WrapForJNI; 22 import org.mozilla.gecko.mozglue.JNIObject; 23 import org.mozilla.gecko.util.BundleEventListener; 24 import org.mozilla.gecko.util.EventCallback; 25 import org.mozilla.gecko.util.GeckoBundle; 26 import org.mozilla.gecko.util.ThreadUtils; 27 import org.mozilla.geckoview.BuildConfig; 28 import org.mozilla.geckoview.GeckoResult; 29 30 @RobocopTarget 31 public final class EventDispatcher extends JNIObject { 32 private static final String LOGTAG = "GeckoEventDispatcher"; 33 34 private static final EventDispatcher INSTANCE = new EventDispatcher(); 35 36 /** 37 * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size of the map 38 * goes beyond 75% of the capacity, the map is rehashed. Therefore, to empirically determine the 39 * initial capacity that avoids rehashing, we need to determine the initial size, divide it by 40 * 75%, and round up to the next power-of-2. 41 */ 42 private static final int DEFAULT_UI_EVENTS_COUNT = 128; // Empirically measured 43 44 private static class Message { 45 final String type; 46 final GeckoBundle bundle; 47 final EventCallback callback; 48 49 Message(final String type, final GeckoBundle bundle, final EventCallback callback) { 50 this.type = type; 51 this.bundle = bundle; 52 this.callback = callback; 53 } 54 } 55 56 // GeckoBundle-based events. 57 private final MultiMap<String, BundleEventListener> mListeners = 58 new MultiMap<>(DEFAULT_UI_EVENTS_COUNT); 59 private Deque<Message> mPendingMessages = new ArrayDeque<>(); 60 61 private boolean mAttachedToGecko; 62 private final NativeQueue mNativeQueue; 63 private final String mName; 64 65 private static Map<String, EventDispatcher> sDispatchers = new HashMap<>(); 66 67 @ReflectionTarget 68 @WrapForJNI(calledFrom = "gecko") 69 public static EventDispatcher getInstance() { 70 return INSTANCE; 71 } 72 73 /** 74 * Gets a named EventDispatcher. 75 * 76 * <p>Named EventDispatchers can be used to communicate to Gecko's corresponding named 77 * EventDispatcher. 78 * 79 * <p>Messages for named EventDispatcher are queued by default when no listener is present. Queued 80 * messages will be released automatically when a listener is attached. 81 * 82 * <p>A named EventDispatcher needs to be disposed manually by calling {@link #shutdown} when it 83 * is not needed anymore. 84 * 85 * @param name Name for this EventDispatcher. 86 * @return the existing named EventDispatcher for a given name or a newly created one if it 87 * doesn't exist. 88 */ 89 @ReflectionTarget 90 @WrapForJNI(calledFrom = "gecko") 91 public static EventDispatcher byName(final String name) { 92 synchronized (sDispatchers) { 93 EventDispatcher dispatcher = sDispatchers.get(name); 94 95 if (dispatcher == null) { 96 dispatcher = new EventDispatcher(name); 97 sDispatchers.put(name, dispatcher); 98 } 99 100 return dispatcher; 101 } 102 } 103 104 /* package */ EventDispatcher() { 105 mNativeQueue = GeckoThread.getNativeQueue(); 106 mName = null; 107 } 108 109 /* package */ EventDispatcher(final String name) { 110 mNativeQueue = GeckoThread.getNativeQueue(); 111 mName = name; 112 } 113 114 public EventDispatcher(final NativeQueue queue) { 115 mNativeQueue = queue; 116 mName = null; 117 } 118 119 private boolean isReadyForDispatchingToGecko() { 120 return mNativeQueue.isReady(); 121 } 122 123 @WrapForJNI 124 @Override // JNIObject 125 protected native void disposeNative(); 126 127 @WrapForJNI(stubName = "Shutdown") 128 protected native void shutdownNative(); 129 130 @WrapForJNI private static final int DETACHED = 0; 131 @WrapForJNI private static final int ATTACHED = 1; 132 @WrapForJNI private static final int REATTACHING = 2; 133 134 @WrapForJNI(calledFrom = "gecko") 135 private synchronized void setAttachedToGecko(final int state) { 136 if (mAttachedToGecko && state == DETACHED) { 137 dispose(false); 138 } 139 mAttachedToGecko = (state == ATTACHED); 140 } 141 142 /** 143 * Shuts down this EventDispatcher and release resources. 144 * 145 * <p>Only named EventDispatcher can be shut down manually. A shut down EventDispatcher will not 146 * receive any further messages. 147 */ 148 public void shutdown() { 149 if (mName == null) { 150 throw new RuntimeException("Only named EventDispatcher's can be shut down."); 151 } 152 153 mAttachedToGecko = false; 154 shutdownNative(); 155 dispose(false); 156 157 synchronized (sDispatchers) { 158 sDispatchers.put(mName, null); 159 } 160 } 161 162 private void dispose(final boolean force) { 163 final Handler geckoHandler = ThreadUtils.sGeckoHandler; 164 if (geckoHandler == null) { 165 return; 166 } 167 168 geckoHandler.post( 169 new Runnable() { 170 @Override 171 public void run() { 172 if (force || !mAttachedToGecko) { 173 disposeNative(); 174 } 175 } 176 }); 177 } 178 179 public void registerUiThreadListener(final BundleEventListener listener, final String... events) { 180 try { 181 synchronized (mListeners) { 182 for (final String event : events) { 183 if (!BuildConfig.RELEASE_OR_BETA && mListeners.containsEntry(event, listener)) { 184 throw new IllegalStateException("Already registered " + event); 185 } 186 mListeners.add(event, listener); 187 } 188 flush(events); 189 } 190 } catch (final Exception e) { 191 throw new IllegalArgumentException("Invalid new list type", e); 192 } 193 } 194 195 public void unregisterUiThreadListener( 196 final BundleEventListener listener, final String... events) { 197 synchronized (mListeners) { 198 for (final String event : events) { 199 if (!mListeners.remove(event, listener) && !BuildConfig.RELEASE_OR_BETA) { 200 throw new IllegalArgumentException(event + " was not registered"); 201 } 202 } 203 } 204 } 205 206 @WrapForJNI 207 private native boolean hasGeckoListener(final String event); 208 209 @WrapForJNI(dispatchTo = "gecko") 210 private native void dispatchToGecko( 211 final String event, final GeckoBundle data, final EventCallback callback); 212 213 /** 214 * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners). 215 * 216 * @param type Event type 217 * @param message Bundle message 218 */ 219 public void dispatch(final String type, final GeckoBundle message) { 220 dispatch(type, message, /* callback */ null); 221 } 222 223 private abstract class CallbackResult<T> extends GeckoResult<T> implements EventCallback { 224 @Override 225 public void sendError(final Object response) { 226 completeExceptionally(new QueryException(response)); 227 } 228 } 229 230 public static class QueryException extends Exception { 231 public final Object data; 232 233 public QueryException(final Object data) { 234 this.data = data; 235 } 236 } 237 238 /** 239 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 240 * 241 * <p>The returned GeckoResult completes when the event handler returns. 242 * 243 * @param type Event type 244 */ 245 public GeckoResult<Void> queryVoid(final String type) { 246 return queryVoid(type, null); 247 } 248 249 /** 250 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 251 * 252 * <p>The returned GeckoResult completes when the event handler returns. 253 * 254 * @param type Event type 255 * @param message GeckoBundle message 256 */ 257 public GeckoResult<Void> queryVoid(final String type, final GeckoBundle message) { 258 return query(type, message); 259 } 260 261 /** 262 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 263 * 264 * <p>The returned GeckoResult completes with the given boolean value returned by the handler. 265 * 266 * @param type Event type 267 */ 268 public GeckoResult<Boolean> queryBoolean(final String type) { 269 return queryBoolean(type, null); 270 } 271 272 /** 273 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 274 * 275 * <p>The returned GeckoResult completes with the given boolean value returned by the handler. 276 * 277 * @param type Event type 278 * @param message GeckoBundle message 279 */ 280 public GeckoResult<Boolean> queryBoolean(final String type, final GeckoBundle message) { 281 return query(type, message); 282 } 283 284 /** 285 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 286 * 287 * <p>The returned GeckoResult completes with the given String value returned by the handler. 288 * 289 * @param type Event type 290 */ 291 public GeckoResult<String> queryString(final String type) { 292 return queryString(type, null); 293 } 294 295 /** 296 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 297 * 298 * <p>The returned GeckoResult completes with the given String value returned by the handler. 299 * 300 * @param type Event type 301 * @param message GeckoBundle message 302 */ 303 public GeckoResult<String> queryString(final String type, final GeckoBundle message) { 304 return query(type, message); 305 } 306 307 /** 308 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 309 * 310 * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the 311 * handler. 312 * 313 * @param type Event type 314 */ 315 public GeckoResult<GeckoBundle> queryBundle(final String type) { 316 return queryBundle(type, null); 317 } 318 319 /** 320 * Query event to any registered Bundle listeners (non-Gecko thread listeners). 321 * 322 * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the 323 * handler. 324 * 325 * @param type Event type 326 * @param message GeckoBundle message 327 */ 328 public GeckoResult<GeckoBundle> queryBundle(final String type, final GeckoBundle message) { 329 return query(type, message); 330 } 331 332 private <T> GeckoResult<T> query(final String type, final GeckoBundle message) { 333 final CallbackResult<T> result = 334 new CallbackResult<T>() { 335 @Override 336 @SuppressWarnings("unchecked") // Not a lot we can do about this :( 337 public void sendSuccess(final Object response) { 338 complete((T) response); 339 } 340 }; 341 342 dispatch(type, message, result); 343 return result; 344 } 345 346 /** 347 * Flushes pending messages of given types. 348 * 349 * <p>All unhandled messages are put into a pending state by default for named EventDispatcher 350 * obtained from {@link #byName}. 351 * 352 * @param types Types of message to flush. 353 */ 354 private void flush(final String[] types) { 355 final Set<String> typeSet = new HashSet<>(Arrays.asList(types)); 356 357 final Deque<Message> pendingMessages; 358 synchronized (mPendingMessages) { 359 pendingMessages = mPendingMessages; 360 mPendingMessages = new ArrayDeque<>(pendingMessages.size()); 361 } 362 363 Message message; 364 while (!pendingMessages.isEmpty()) { 365 message = pendingMessages.removeFirst(); 366 if (typeSet.contains(message.type)) { 367 dispatchToThreads(message.type, message.bundle, message.callback); 368 } else { 369 synchronized (mPendingMessages) { 370 mPendingMessages.addLast(message); 371 } 372 } 373 } 374 } 375 376 /** 377 * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners). 378 * 379 * @param type Event type 380 * @param message Bundle message 381 * @param callback Optional object for callbacks from events. 382 */ 383 @AnyThread 384 private void dispatch( 385 final String type, final GeckoBundle message, final EventCallback callback) { 386 final boolean isGeckoReady; 387 synchronized (this) { 388 isGeckoReady = isReadyForDispatchingToGecko(); 389 if (isGeckoReady && mAttachedToGecko && hasGeckoListener(type)) { 390 dispatchToGecko(type, message, JavaCallbackDelegate.wrap(callback)); 391 return; 392 } 393 } 394 395 dispatchToThreads(type, message, callback, isGeckoReady); 396 } 397 398 @WrapForJNI(calledFrom = "gecko") 399 private boolean dispatchToThreads( 400 final String type, final GeckoBundle message, final EventCallback callback) { 401 return dispatchToThreads(type, message, callback, /* isGeckoReady */ true); 402 } 403 404 private boolean dispatchToThreads( 405 final String type, 406 final GeckoBundle message, 407 final EventCallback callback, 408 final boolean isGeckoReady) { 409 // We need to hold the lock throughout dispatching, to ensure the listeners list 410 // is consistent, while we iterate over it. We don't have to worry about listeners 411 // running for a long time while we have the lock, because the listeners will run 412 // on a separate thread. 413 synchronized (mListeners) { 414 if (mListeners.containsKey(type)) { 415 // Use a delegate to make sure callbacks happen on a specific thread. 416 final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback); 417 418 // Event listeners will call | callback.sendError | if applicable. 419 for (final BundleEventListener listener : mListeners.get(type)) { 420 ThreadUtils.getUiHandler() 421 .post( 422 new Runnable() { 423 @Override 424 public void run() { 425 final Double startTime = GeckoJavaSampler.tryToGetProfilerTime(); 426 listener.handleMessage(type, message, wrappedCallback); 427 GeckoJavaSampler.addMarker( 428 "EventDispatcher handleMessage", startTime, null, type); 429 } 430 }); 431 } 432 return true; 433 } 434 } 435 436 if (!isGeckoReady) { 437 // Usually, we discard an event if there is no listeners for it by 438 // the time of the dispatch. However, if Gecko(View) is not ready and 439 // there is no listener for this event that's possibly headed to 440 // Gecko, we make a special exception to queue this event until 441 // Gecko(View) is ready. This way, Gecko can first register its 442 // listeners, and accept the event when it is ready. 443 mNativeQueue.queueUntilReady( 444 this, 445 "dispatchToGecko", 446 String.class, 447 type, 448 GeckoBundle.class, 449 message, 450 EventCallback.class, 451 JavaCallbackDelegate.wrap(callback)); 452 return true; 453 } 454 455 // Named EventDispatchers use pending messages 456 if (mName != null) { 457 synchronized (mPendingMessages) { 458 mPendingMessages.addLast(new Message(type, message, callback)); 459 } 460 return true; 461 } 462 463 final String error = "No listener for " + type; 464 if (callback != null) { 465 callback.sendError(error); 466 } 467 468 Log.w(LOGTAG, error); 469 return false; 470 } 471 472 @WrapForJNI 473 public boolean hasListener(final String event) { 474 synchronized (mListeners) { 475 return mListeners.containsKey(event); 476 } 477 } 478 479 @Override 480 protected void finalize() throws Throwable { 481 dispose(true); 482 } 483 484 private static class NativeCallbackDelegate extends JNIObject implements EventCallback { 485 @WrapForJNI(calledFrom = "gecko") 486 private NativeCallbackDelegate() {} 487 488 @Override // JNIObject 489 protected void disposeNative() { 490 // We dispose in finalize(). 491 throw new UnsupportedOperationException(); 492 } 493 494 @WrapForJNI(dispatchTo = "proxy") 495 @Override // EventCallback 496 public native void sendSuccess(Object response); 497 498 @WrapForJNI(dispatchTo = "proxy") 499 @Override // EventCallback 500 public native void sendError(Object response); 501 502 @WrapForJNI(dispatchTo = "gecko") 503 @Override // Object 504 protected native void finalize(); 505 } 506 507 private static class JavaCallbackDelegate implements EventCallback { 508 private final Thread mOriginalThread = Thread.currentThread(); 509 private final EventCallback mCallback; 510 511 public static EventCallback wrap(final EventCallback callback) { 512 if (callback == null) { 513 return null; 514 } 515 if (callback instanceof NativeCallbackDelegate) { 516 // NativeCallbackDelegate always posts to Gecko thread if needed. 517 return callback; 518 } 519 return new JavaCallbackDelegate(callback); 520 } 521 522 JavaCallbackDelegate(final EventCallback callback) { 523 mCallback = callback; 524 } 525 526 private void makeCallback(final boolean callSuccess, final Object rawResponse) { 527 final Object response; 528 if (rawResponse instanceof Number) { 529 // There is ambiguity because a number can be converted to either int or 530 // double, so e.g. the user can be expecting a double when we give it an 531 // int. To avoid these pitfalls, we disallow all numbers. The workaround 532 // is to wrap the number in a JS object / GeckoBundle, which supports 533 // type coersion for numbers. 534 throw new UnsupportedOperationException("Cannot use number as Java callback result"); 535 } else if (rawResponse != null && rawResponse.getClass().isArray()) { 536 // Same with arrays. 537 throw new UnsupportedOperationException("Cannot use arrays as Java callback result"); 538 } else if (rawResponse instanceof Character) { 539 response = rawResponse.toString(); 540 } else { 541 response = rawResponse; 542 } 543 544 // Call back synchronously if we happen to be on the same thread as the thread 545 // making the original request. 546 if (ThreadUtils.isOnThread(mOriginalThread)) { 547 if (callSuccess) { 548 mCallback.sendSuccess(response); 549 } else { 550 mCallback.sendError(response); 551 } 552 return; 553 } 554 555 // Make callback on the thread of the original request, if the original thread 556 // is the UI or Gecko thread. Otherwise default to the background thread. 557 final Handler handler = 558 mOriginalThread == ThreadUtils.getUiThread() 559 ? ThreadUtils.getUiHandler() 560 : mOriginalThread == ThreadUtils.sGeckoThread 561 ? ThreadUtils.sGeckoHandler 562 : ThreadUtils.getBackgroundHandler(); 563 final EventCallback callback = mCallback; 564 565 handler.post( 566 new Runnable() { 567 @Override 568 public void run() { 569 if (callSuccess) { 570 callback.sendSuccess(response); 571 } else { 572 callback.sendError(response); 573 } 574 } 575 }); 576 } 577 578 @Override // EventCallback 579 public void sendSuccess(final Object response) { 580 makeCallback(/* success */ true, response); 581 } 582 583 @Override // EventCallback 584 public void sendError(final Object response) { 585 makeCallback(/* success */ false, response); 586 } 587 } 588 }