diff options
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.java | 503 |
1 files changed, 503 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 000000000..6c4e67b43 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java @@ -0,0 +1,503 @@ +/* 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 org.mozilla.gecko.annotation.ReflectionTarget; +import org.mozilla.gecko.annotation.RobocopTarget; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.NativeEventListener; +import org.mozilla.gecko.util.NativeJSContainer; +import org.mozilla.gecko.util.NativeJSObject; +import org.mozilla.gecko.util.ThreadUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +@RobocopTarget +public final class EventDispatcher { + private static final String LOGTAG = "GeckoEventDispatcher"; + /* package */ static final String GUID = "__guid__"; + private static final String STATUS_ERROR = "error"; + private static final String STATUS_SUCCESS = "success"; + + 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_GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap + private static final int DEFAULT_GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured + private static final int DEFAULT_UI_EVENTS_COUNT = 0; // Default for HashMap + private static final int DEFAULT_BACKGROUND_EVENTS_COUNT = 0; // Default for HashMap + + private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners = + new HashMap<String, List<NativeEventListener>>(DEFAULT_GECKO_NATIVE_EVENTS_COUNT); + private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners = + new HashMap<String, List<GeckoEventListener>>(DEFAULT_GECKO_JSON_EVENTS_COUNT); + private final Map<String, List<BundleEventListener>> mUiThreadListeners = + new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT); + private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners = + new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT); + + @ReflectionTarget + public static EventDispatcher getInstance() { + return INSTANCE; + } + + public EventDispatcher() { + } + + private <T> void registerListener(final Class<?> listType, + final Map<String, List<T>> listenersMap, + final T listener, + final String[] events) { + try { + synchronized (listenersMap) { + for (final String event : events) { + List<T> listeners = listenersMap.get(event); + if (listeners == null) { + // Java doesn't let us put Class<? extends List<T>> as the type for listType. + @SuppressWarnings("unchecked") + final Class<? extends List<T>> type = (Class) listType; + listeners = type.newInstance(); + listenersMap.put(event, listeners); + } + if (!AppConstants.RELEASE_OR_BETA && listeners.contains(listener)) { + throw new IllegalStateException("Already registered " + event); + } + listeners.add(listener); + } + } + } catch (final IllegalAccessException | InstantiationException e) { + throw new IllegalArgumentException("Invalid new list type", e); + } + } + + private void checkNotRegisteredElsewhere(final Map<String, ?> allowedMap, + final String[] events) { + if (AppConstants.RELEASE_OR_BETA) { + // for performance reasons, we only check for + // already-registered listeners in non-release builds. + return; + } + for (final Map<String, ?> listenersMap : Arrays.asList(mGeckoThreadNativeListeners, + mGeckoThreadJSONListeners, + mUiThreadListeners, + mBackgroundThreadListeners)) { + if (listenersMap == allowedMap) { + continue; + } + synchronized (listenersMap) { + for (final String event : events) { + if (listenersMap.get(event) != null) { + throw new IllegalStateException( + "Already registered " + event + " under a different type"); + } + } + } + } + } + + private <T> void unregisterListener(final Map<String, List<T>> listenersMap, + final T listener, + final String[] events) { + synchronized (listenersMap) { + for (final String event : events) { + List<T> listeners = listenersMap.get(event); + if ((listeners == null || + !listeners.remove(listener)) && !AppConstants.RELEASE_OR_BETA) { + throw new IllegalArgumentException(event + " was not registered"); + } + } + } + } + + public void registerGeckoThreadListener(final NativeEventListener listener, + final String... events) { + checkNotRegisteredElsewhere(mGeckoThreadNativeListeners, events); + + // For listeners running on the Gecko thread, we want to notify the listeners + // outside of our synchronized block, because the listeners may take an + // indeterminate amount of time to run. Therefore, to ensure concurrency when + // iterating the list outside of the synchronized block, we use a + // CopyOnWriteArrayList. + registerListener(CopyOnWriteArrayList.class, + mGeckoThreadNativeListeners, listener, events); + } + + @Deprecated // Use NativeEventListener instead + public void registerGeckoThreadListener(final GeckoEventListener listener, + final String... events) { + checkNotRegisteredElsewhere(mGeckoThreadJSONListeners, events); + + registerListener(CopyOnWriteArrayList.class, + mGeckoThreadJSONListeners, listener, events); + } + + public void registerUiThreadListener(final BundleEventListener listener, + final String... events) { + checkNotRegisteredElsewhere(mUiThreadListeners, events); + + registerListener(ArrayList.class, + mUiThreadListeners, listener, events); + } + + @ReflectionTarget + public void registerBackgroundThreadListener(final BundleEventListener listener, + final String... events) { + checkNotRegisteredElsewhere(mBackgroundThreadListeners, events); + + registerListener(ArrayList.class, + mBackgroundThreadListeners, listener, events); + } + + public void unregisterGeckoThreadListener(final NativeEventListener listener, + final String... events) { + unregisterListener(mGeckoThreadNativeListeners, listener, events); + } + + @Deprecated // Use NativeEventListener instead + public void unregisterGeckoThreadListener(final GeckoEventListener listener, + final String... events) { + unregisterListener(mGeckoThreadJSONListeners, listener, events); + } + + public void unregisterUiThreadListener(final BundleEventListener listener, + final String... events) { + unregisterListener(mUiThreadListeners, listener, events); + } + + public void unregisterBackgroundThreadListener(final BundleEventListener listener, + final String... events) { + unregisterListener(mBackgroundThreadListeners, listener, events); + } + + private List<NativeEventListener> getNativeListeners(final String type) { + final List<NativeEventListener> listeners; + synchronized (mGeckoThreadNativeListeners) { + listeners = mGeckoThreadNativeListeners.get(type); + } + return listeners; + } + + private List<GeckoEventListener> getGeckoListeners(final String type) { + final List<GeckoEventListener> listeners; + synchronized (mGeckoThreadJSONListeners) { + listeners = mGeckoThreadJSONListeners.get(type); + } + return listeners; + } + + public boolean dispatchEvent(final NativeJSContainer message) { + // First try native listeners. + final String type = message.optString("type", null); + if (type == null) { + Log.e(LOGTAG, "JSON message must have a type property"); + return true; // It may seem odd to return true here, but it's necessary to preserve the correct behavior. + } + + final List<NativeEventListener> listeners = getNativeListeners(type); + + final String guid = message.optString(GUID, null); + EventCallback callback = null; + if (guid != null) { + callback = new GeckoEventCallback(guid, type); + } + + if (listeners != null) { + if (listeners.isEmpty()) { + Log.w(LOGTAG, "No listeners for " + type); + + // There were native listeners, and they're gone. Return a failure rather than + // looking for JSON listeners. This is an optimization, as we can safely assume + // that an event which previously had native listeners will never have JSON + // listeners. + return false; + } + try { + for (final NativeEventListener listener : listeners) { + listener.handleMessage(type, message, callback); + } + } catch (final NativeJSObject.InvalidPropertyException e) { + Log.e(LOGTAG, "Exception occurred while handling " + type, e); + } + // If we found native listeners, we assume we don't have any other types of listeners + // and return early. This assumption is checked when registering listeners. + return true; + } + + // Check for thread event listeners before checking for JSON event listeners, + // because checking for thread listeners is very fast and doesn't require us to + // serialize into JSON and construct a JSONObject. + if (dispatchToThreads(type, message, /* bundle */ null, callback)) { + // If we found thread listeners, we assume we don't have any other types of listeners + // and return early. This assumption is checked when registering listeners. + return true; + } + + try { + // If we didn't find native listeners, try JSON listeners. + return dispatchEvent(new JSONObject(message.toString()), callback); + } catch (final JSONException e) { + Log.e(LOGTAG, "Cannot parse JSON", e); + } catch (final UnsupportedOperationException e) { + Log.e(LOGTAG, "Cannot convert message to JSON", e); + } + + return true; + } + + /** + * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners). + * + * @param message Bundle message with "type" value specifying the event type. + */ + public void dispatch(final Bundle message) { + dispatch(message, /* callback */ null); + } + + /** + * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners). + * + * @param message Bundle message with "type" value specifying the event type. + * @param callback Optional object for callbacks from events. + */ + public void dispatch(final Bundle message, final EventCallback callback) { + if (message == null) { + throw new IllegalArgumentException("Null message"); + } + + final String type = message.getCharSequence("type").toString(); + if (type == null) { + Log.e(LOGTAG, "Bundle message must have a type property"); + return; + } + dispatchToThreads(type, /* js */ null, message, /* callback */ 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 Bundle message) { + dispatch(type, message, /* callback */ null); + } + + /** + * 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. + */ + public void dispatch(final String type, final Bundle message, final EventCallback callback) { + dispatchToThreads(type, /* js */ null, message, /* callback */ callback); + } + + private boolean dispatchToThreads(final String type, + final NativeJSObject jsMessage, + final Bundle bundleMessage, + final EventCallback callback) { + if (dispatchToThread(type, jsMessage, bundleMessage, callback, + mUiThreadListeners, ThreadUtils.getUiHandler())) { + return true; + } + + if (dispatchToThread(type, jsMessage, bundleMessage, callback, + mBackgroundThreadListeners, ThreadUtils.getBackgroundHandler())) { + return true; + } + + if (jsMessage == null) { + Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThreads"); + } + + if (!AppConstants.RELEASE_OR_BETA && jsMessage == null) { + // We're dispatching a Bundle message. Because Gecko thread listeners are not + // supported for Bundle messages, do a sanity check to make sure we don't have + // matching Gecko thread listeners. + boolean hasGeckoListener = false; + synchronized (mGeckoThreadNativeListeners) { + hasGeckoListener |= mGeckoThreadNativeListeners.containsKey(type); + } + synchronized (mGeckoThreadJSONListeners) { + hasGeckoListener |= mGeckoThreadJSONListeners.containsKey(type); + } + if (hasGeckoListener) { + throw new IllegalStateException( + "Dispatching Bundle message to Gecko listener " + type); + } + } + + return false; + } + + private boolean dispatchToThread(final String type, + final NativeJSObject jsMessage, + final Bundle bundleMessage, + final EventCallback callback, + final Map<String, List<BundleEventListener>> listenersMap, + final Handler thread) { + // 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 (listenersMap) { + final List<BundleEventListener> listeners = listenersMap.get(type); + if (listeners == null) { + return false; + } + + if (listeners.isEmpty()) { + Log.w(LOGTAG, "No listeners for " + type + " in dispatchToThread"); + + // There were native listeners, and they're gone. + return false; + } + + final Bundle messageAsBundle; + try { + messageAsBundle = jsMessage != null ? jsMessage.toBundle() : bundleMessage; + } catch (final NativeJSObject.InvalidPropertyException e) { + Log.e(LOGTAG, "Exception occurred while handling " + type, e); + return true; + } + + // Event listeners will call | callback.sendError | if applicable. + for (final BundleEventListener listener : listeners) { + thread.post(new Runnable() { + @Override + public void run() { + listener.handleMessage(type, messageAsBundle, callback); + } + }); + } + return true; + } + } + + public boolean dispatchEvent(final JSONObject message, final EventCallback callback) { + // { + // "type": "value", + // "event_specific": "value", + // ... + try { + final String type = message.getString("type"); + + final List<GeckoEventListener> listeners = getGeckoListeners(type); + + if (listeners == null || listeners.isEmpty()) { + Log.w(LOGTAG, "No listeners for " + type + " in dispatchEvent"); + + return false; + } + + for (final GeckoEventListener listener : listeners) { + listener.handleMessage(type, message); + } + } catch (final JSONException e) { + Log.e(LOGTAG, "handleGeckoMessage throws " + e, e); + } + + return true; + } + + @RobocopTarget + @Deprecated + public static void sendResponse(JSONObject message, Object response) { + sendResponseHelper(STATUS_SUCCESS, message, response); + } + + @Deprecated + public static void sendError(JSONObject message, Object response) { + sendResponseHelper(STATUS_ERROR, message, response); + } + + @Deprecated + private static void sendResponseHelper(String status, JSONObject message, Object response) { + try { + final String topic = message.getString("type") + ":Response"; + final JSONObject wrapper = new JSONObject(); + wrapper.put(GUID, message.getString(GUID)); + wrapper.put("status", status); + wrapper.put("response", response); + + if (ThreadUtils.isOnGeckoThread()) { + GeckoAppShell.syncNotifyObservers(topic, wrapper.toString()); + } else { + GeckoAppShell.notifyObservers(topic, wrapper.toString(), + GeckoThread.State.PROFILE_READY); + } + } catch (final JSONException e) { + Log.e(LOGTAG, "Unable to send response", e); + } + } + + /* package */ static class GeckoEventCallback implements EventCallback { + private final String guid; + private final String type; + private boolean sent; + + public GeckoEventCallback(final String guid, final String type) { + this.guid = guid; + this.type = type; + } + + @Override + public void sendSuccess(final Object response) { + sendResponse(STATUS_SUCCESS, response); + } + + @Override + public void sendError(final Object response) { + sendResponse(STATUS_ERROR, response); + } + + private void sendResponse(final String status, final Object response) { + if (sent) { + throw new IllegalStateException("Callback has already been executed for type=" + + type + ", guid=" + guid); + } + + sent = true; + + try { + final String topic = type + ":Response"; + final JSONObject wrapper = new JSONObject(); + wrapper.put(GUID, guid); + wrapper.put("status", status); + wrapper.put("response", response); + + if (ThreadUtils.isOnGeckoThread()) { + GeckoAppShell.syncNotifyObservers(topic, wrapper.toString()); + } else { + GeckoAppShell.notifyObservers(topic, wrapper.toString(), + GeckoThread.State.PROFILE_READY); + } + } catch (final JSONException e) { + Log.e(LOGTAG, "Unable to send response for: " + type, e); + } + } + } +} |