summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java588
1 files changed, 588 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
new file mode 100644
index 0000000000..647ac5bc09
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -0,0 +1,588 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+/* 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/. */
+
+package org.mozilla.gecko;
+
+import android.os.Handler;
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.geckoview.GeckoResult;
+
+@RobocopTarget
+public final class EventDispatcher extends JNIObject {
+ private static final String LOGTAG = "GeckoEventDispatcher";
+
+ private static final EventDispatcher INSTANCE = new EventDispatcher();
+
+ /**
+ * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size of the map
+ * goes beyond 75% of the capacity, the map is rehashed. Therefore, to empirically determine the
+ * initial capacity that avoids rehashing, we need to determine the initial size, divide it by
+ * 75%, and round up to the next power-of-2.
+ */
+ private static final int DEFAULT_UI_EVENTS_COUNT = 128; // Empirically measured
+
+ private static class Message {
+ final String type;
+ final GeckoBundle bundle;
+ final EventCallback callback;
+
+ Message(final String type, final GeckoBundle bundle, final EventCallback callback) {
+ this.type = type;
+ this.bundle = bundle;
+ this.callback = callback;
+ }
+ }
+
+ // GeckoBundle-based events.
+ private final MultiMap<String, BundleEventListener> mListeners =
+ new MultiMap<>(DEFAULT_UI_EVENTS_COUNT);
+ private Deque<Message> mPendingMessages = new ArrayDeque<>();
+
+ private boolean mAttachedToGecko;
+ private final NativeQueue mNativeQueue;
+ private final String mName;
+
+ private static Map<String, EventDispatcher> sDispatchers = new HashMap<>();
+
+ @ReflectionTarget
+ @WrapForJNI(calledFrom = "gecko")
+ public static EventDispatcher getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Gets a named EventDispatcher.
+ *
+ * <p>Named EventDispatchers can be used to communicate to Gecko's corresponding named
+ * EventDispatcher.
+ *
+ * <p>Messages for named EventDispatcher are queued by default when no listener is present. Queued
+ * messages will be released automatically when a listener is attached.
+ *
+ * <p>A named EventDispatcher needs to be disposed manually by calling {@link #shutdown} when it
+ * is not needed anymore.
+ *
+ * @param name Name for this EventDispatcher.
+ * @return the existing named EventDispatcher for a given name or a newly created one if it
+ * doesn't exist.
+ */
+ @ReflectionTarget
+ @WrapForJNI(calledFrom = "gecko")
+ public static EventDispatcher byName(final String name) {
+ synchronized (sDispatchers) {
+ EventDispatcher dispatcher = sDispatchers.get(name);
+
+ if (dispatcher == null) {
+ dispatcher = new EventDispatcher(name);
+ sDispatchers.put(name, dispatcher);
+ }
+
+ return dispatcher;
+ }
+ }
+
+ /* package */ EventDispatcher() {
+ mNativeQueue = GeckoThread.getNativeQueue();
+ mName = null;
+ }
+
+ /* package */ EventDispatcher(final String name) {
+ mNativeQueue = GeckoThread.getNativeQueue();
+ mName = name;
+ }
+
+ public EventDispatcher(final NativeQueue queue) {
+ mNativeQueue = queue;
+ mName = null;
+ }
+
+ private boolean isReadyForDispatchingToGecko() {
+ return mNativeQueue.isReady();
+ }
+
+ @WrapForJNI
+ @Override // JNIObject
+ protected native void disposeNative();
+
+ @WrapForJNI(stubName = "Shutdown")
+ protected native void shutdownNative();
+
+ @WrapForJNI private static final int DETACHED = 0;
+ @WrapForJNI private static final int ATTACHED = 1;
+ @WrapForJNI private static final int REATTACHING = 2;
+
+ @WrapForJNI(calledFrom = "gecko")
+ private synchronized void setAttachedToGecko(final int state) {
+ if (mAttachedToGecko && state == DETACHED) {
+ dispose(false);
+ }
+ mAttachedToGecko = (state == ATTACHED);
+ }
+
+ /**
+ * Shuts down this EventDispatcher and release resources.
+ *
+ * <p>Only named EventDispatcher can be shut down manually. A shut down EventDispatcher will not
+ * receive any further messages.
+ */
+ public void shutdown() {
+ if (mName == null) {
+ throw new RuntimeException("Only named EventDispatcher's can be shut down.");
+ }
+
+ mAttachedToGecko = false;
+ shutdownNative();
+ dispose(false);
+
+ synchronized (sDispatchers) {
+ sDispatchers.put(mName, null);
+ }
+ }
+
+ private void dispose(final boolean force) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ if (geckoHandler == null) {
+ return;
+ }
+
+ geckoHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (force || !mAttachedToGecko) {
+ disposeNative();
+ }
+ }
+ });
+ }
+
+ public void registerUiThreadListener(final BundleEventListener listener, final String... events) {
+ try {
+ synchronized (mListeners) {
+ for (final String event : events) {
+ if (!BuildConfig.RELEASE_OR_BETA && mListeners.containsEntry(event, listener)) {
+ throw new IllegalStateException("Already registered " + event);
+ }
+ mListeners.add(event, listener);
+ }
+ flush(events);
+ }
+ } catch (final Exception e) {
+ throw new IllegalArgumentException("Invalid new list type", e);
+ }
+ }
+
+ public void unregisterUiThreadListener(
+ final BundleEventListener listener, final String... events) {
+ synchronized (mListeners) {
+ for (final String event : events) {
+ if (!mListeners.remove(event, listener) && !BuildConfig.RELEASE_OR_BETA) {
+ throw new IllegalArgumentException(event + " was not registered");
+ }
+ }
+ }
+ }
+
+ @WrapForJNI
+ private native boolean hasGeckoListener(final String event);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private native void dispatchToGecko(
+ final String event, final GeckoBundle data, final EventCallback callback);
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ */
+ public void dispatch(final String type, final GeckoBundle message) {
+ dispatch(type, message, /* callback */ null);
+ }
+
+ private abstract class CallbackResult<T> extends GeckoResult<T> implements EventCallback {
+ @Override
+ public void sendError(final Object response) {
+ completeExceptionally(new QueryException(response));
+ }
+ }
+
+ public class QueryException extends Exception {
+ public final Object data;
+
+ public QueryException(final Object data) {
+ this.data = data;
+ }
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes when the event handler returns.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<Void> queryVoid(final String type) {
+ return queryVoid(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes when the event handler returns.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<Void> queryVoid(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given boolean value returned by the handler.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<Boolean> queryBoolean(final String type) {
+ return queryBoolean(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given boolean value returned by the handler.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<Boolean> queryBoolean(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given String value returned by the handler.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<String> queryString(final String type) {
+ return queryString(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given String value returned by the handler.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<String> queryString(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the
+ * handler.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<GeckoBundle> queryBundle(final String type) {
+ return queryBundle(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the
+ * handler.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<GeckoBundle> queryBundle(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ private <T> GeckoResult<T> query(final String type, final GeckoBundle message) {
+ final CallbackResult<T> result =
+ new CallbackResult<T>() {
+ @Override
+ @SuppressWarnings("unchecked") // Not a lot we can do about this :(
+ public void sendSuccess(final Object response) {
+ complete((T) response);
+ }
+ };
+
+ dispatch(type, message, result);
+ return result;
+ }
+
+ /**
+ * Flushes pending messages of given types.
+ *
+ * <p>All unhandled messages are put into a pending state by default for named EventDispatcher
+ * obtained from {@link #byName}.
+ *
+ * @param types Types of message to flush.
+ */
+ private void flush(final String[] types) {
+ final Set<String> typeSet = new HashSet<>(Arrays.asList(types));
+
+ final Deque<Message> pendingMessages;
+ synchronized (mPendingMessages) {
+ pendingMessages = mPendingMessages;
+ mPendingMessages = new ArrayDeque<>(pendingMessages.size());
+ }
+
+ Message message;
+ while (!pendingMessages.isEmpty()) {
+ message = pendingMessages.removeFirst();
+ if (typeSet.contains(message.type)) {
+ dispatchToThreads(message.type, message.bundle, message.callback);
+ } else {
+ synchronized (mPendingMessages) {
+ mPendingMessages.addLast(message);
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ * @param callback Optional object for callbacks from events.
+ */
+ @AnyThread
+ private void dispatch(
+ final String type, final GeckoBundle message, final EventCallback callback) {
+ final boolean isGeckoReady;
+ synchronized (this) {
+ isGeckoReady = isReadyForDispatchingToGecko();
+ if (isGeckoReady && mAttachedToGecko && hasGeckoListener(type)) {
+ dispatchToGecko(type, message, JavaCallbackDelegate.wrap(callback));
+ return;
+ }
+ }
+
+ dispatchToThreads(type, message, callback, isGeckoReady);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private boolean dispatchToThreads(
+ final String type, final GeckoBundle message, final EventCallback callback) {
+ return dispatchToThreads(type, message, callback, /* isGeckoReady */ true);
+ }
+
+ private boolean dispatchToThreads(
+ final String type,
+ final GeckoBundle message,
+ final EventCallback callback,
+ final boolean isGeckoReady) {
+ // We need to hold the lock throughout dispatching, to ensure the listeners list
+ // is consistent, while we iterate over it. We don't have to worry about listeners
+ // running for a long time while we have the lock, because the listeners will run
+ // on a separate thread.
+ synchronized (mListeners) {
+ if (mListeners.containsKey(type)) {
+ // Use a delegate to make sure callbacks happen on a specific thread.
+ final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
+
+ // Event listeners will call | callback.sendError | if applicable.
+ for (final BundleEventListener listener : mListeners.get(type)) {
+ ThreadUtils.getUiHandler()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ final Double startTime = GeckoJavaSampler.tryToGetProfilerTime();
+ listener.handleMessage(type, message, wrappedCallback);
+ GeckoJavaSampler.addMarker(
+ "EventDispatcher handleMessage", startTime, null, type);
+ }
+ });
+ }
+ return true;
+ }
+ }
+
+ if (!isGeckoReady) {
+ // Usually, we discard an event if there is no listeners for it by
+ // the time of the dispatch. However, if Gecko(View) is not ready and
+ // there is no listener for this event that's possibly headed to
+ // Gecko, we make a special exception to queue this event until
+ // Gecko(View) is ready. This way, Gecko can first register its
+ // listeners, and accept the event when it is ready.
+ mNativeQueue.queueUntilReady(
+ this,
+ "dispatchToGecko",
+ String.class,
+ type,
+ GeckoBundle.class,
+ message,
+ EventCallback.class,
+ JavaCallbackDelegate.wrap(callback));
+ return true;
+ }
+
+ // Named EventDispatchers use pending messages
+ if (mName != null) {
+ synchronized (mPendingMessages) {
+ mPendingMessages.addLast(new Message(type, message, callback));
+ }
+ return true;
+ }
+
+ final String error = "No listener for " + type;
+ if (callback != null) {
+ callback.sendError(error);
+ }
+
+ Log.w(LOGTAG, error);
+ return false;
+ }
+
+ @WrapForJNI
+ public boolean hasListener(final String event) {
+ synchronized (mListeners) {
+ return mListeners.containsKey(event);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ dispose(true);
+ }
+
+ private static class NativeCallbackDelegate extends JNIObject implements EventCallback {
+ @WrapForJNI(calledFrom = "gecko")
+ private NativeCallbackDelegate() {}
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ // We dispose in finalize().
+ throw new UnsupportedOperationException();
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // EventCallback
+ public native void sendSuccess(Object response);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // EventCallback
+ public native void sendError(Object response);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ @Override // Object
+ protected native void finalize();
+ }
+
+ private static class JavaCallbackDelegate implements EventCallback {
+ private final Thread mOriginalThread = Thread.currentThread();
+ private final EventCallback mCallback;
+
+ public static EventCallback wrap(final EventCallback callback) {
+ if (callback == null) {
+ return null;
+ }
+ if (callback instanceof NativeCallbackDelegate) {
+ // NativeCallbackDelegate always posts to Gecko thread if needed.
+ return callback;
+ }
+ return new JavaCallbackDelegate(callback);
+ }
+
+ JavaCallbackDelegate(final EventCallback callback) {
+ mCallback = callback;
+ }
+
+ private void makeCallback(final boolean callSuccess, final Object rawResponse) {
+ final Object response;
+ if (rawResponse instanceof Number) {
+ // There is ambiguity because a number can be converted to either int or
+ // double, so e.g. the user can be expecting a double when we give it an
+ // int. To avoid these pitfalls, we disallow all numbers. The workaround
+ // is to wrap the number in a JS object / GeckoBundle, which supports
+ // type coersion for numbers.
+ throw new UnsupportedOperationException("Cannot use number as Java callback result");
+ } else if (rawResponse != null && rawResponse.getClass().isArray()) {
+ // Same with arrays.
+ throw new UnsupportedOperationException("Cannot use arrays as Java callback result");
+ } else if (rawResponse instanceof Character) {
+ response = rawResponse.toString();
+ } else {
+ response = rawResponse;
+ }
+
+ // Call back synchronously if we happen to be on the same thread as the thread
+ // making the original request.
+ if (ThreadUtils.isOnThread(mOriginalThread)) {
+ if (callSuccess) {
+ mCallback.sendSuccess(response);
+ } else {
+ mCallback.sendError(response);
+ }
+ return;
+ }
+
+ // Make callback on the thread of the original request, if the original thread
+ // is the UI or Gecko thread. Otherwise default to the background thread.
+ final Handler handler =
+ mOriginalThread == ThreadUtils.getUiThread()
+ ? ThreadUtils.getUiHandler()
+ : mOriginalThread == ThreadUtils.sGeckoThread
+ ? ThreadUtils.sGeckoHandler
+ : ThreadUtils.getBackgroundHandler();
+ final EventCallback callback = mCallback;
+
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (callSuccess) {
+ callback.sendSuccess(response);
+ } else {
+ callback.sendError(response);
+ }
+ }
+ });
+ }
+
+ @Override // EventCallback
+ public void sendSuccess(final Object response) {
+ makeCallback(/* success */ true, response);
+ }
+
+ @Override // EventCallback
+ public void sendError(final Object response) {
+ makeCallback(/* success */ false, response);
+ }
+ }
+}