mobile/android/base/EventDispatcher.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/EventDispatcher.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,277 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko;
     1.9 +
    1.10 +import org.mozilla.gecko.GeckoAppShell;
    1.11 +import org.mozilla.gecko.GeckoEvent;
    1.12 +import org.mozilla.gecko.util.EventCallback;
    1.13 +import org.mozilla.gecko.util.GeckoEventListener;
    1.14 +import org.mozilla.gecko.util.NativeEventListener;
    1.15 +import org.mozilla.gecko.util.NativeJSContainer;
    1.16 +
    1.17 +import org.json.JSONException;
    1.18 +import org.json.JSONObject;
    1.19 +
    1.20 +import android.util.Log;
    1.21 +
    1.22 +import java.util.HashMap;
    1.23 +import java.util.List;
    1.24 +import java.util.Map;
    1.25 +import java.util.concurrent.CopyOnWriteArrayList;
    1.26 +
    1.27 +public final class EventDispatcher {
    1.28 +    private static final String LOGTAG = "GeckoEventDispatcher";
    1.29 +    private static final String GUID = "__guid__";
    1.30 +    private static final String STATUS_CANCEL = "cancel";
    1.31 +    private static final String STATUS_ERROR = "error";
    1.32 +    private static final String STATUS_SUCCESS = "success";
    1.33 +
    1.34 +    /**
    1.35 +     * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size
    1.36 +     * of the map goes beyond 75% of the capacity, the map is rehashed. Therefore, to
    1.37 +     * empirically determine the initial capacity that avoids rehashing, we need to
    1.38 +     * determine the initial size, divide it by 75%, and round up to the next power-of-2.
    1.39 +     */
    1.40 +    private static final int GECKO_NATIVE_EVENTS_COUNT = 0; // Default for HashMap
    1.41 +    private static final int GECKO_JSON_EVENTS_COUNT = 256; // Empirically measured
    1.42 +
    1.43 +    private final Map<String, List<NativeEventListener>> mGeckoThreadNativeListeners =
    1.44 +        new HashMap<String, List<NativeEventListener>>(GECKO_NATIVE_EVENTS_COUNT);
    1.45 +    private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners =
    1.46 +        new HashMap<String, List<GeckoEventListener>>(GECKO_JSON_EVENTS_COUNT);
    1.47 +
    1.48 +    private <T> void registerListener(final Class<? extends List<T>> listType,
    1.49 +                                      final Map<String, List<T>> listenersMap,
    1.50 +                                      final T listener,
    1.51 +                                      final String[] events) {
    1.52 +        try {
    1.53 +            synchronized (listenersMap) {
    1.54 +                for (final String event : events) {
    1.55 +                    List<T> listeners = listenersMap.get(event);
    1.56 +                    if (listeners == null) {
    1.57 +                        listeners = listType.newInstance();
    1.58 +                        listenersMap.put(event, listeners);
    1.59 +                    }
    1.60 +                    if (!AppConstants.RELEASE_BUILD && listeners.contains(listener)) {
    1.61 +                        throw new IllegalStateException("Already registered " + event);
    1.62 +                    }
    1.63 +                    listeners.add(listener);
    1.64 +                }
    1.65 +            }
    1.66 +        } catch (final IllegalAccessException e) {
    1.67 +            throw new IllegalArgumentException("Invalid new list type", e);
    1.68 +        } catch (final InstantiationException e) {
    1.69 +            throw new IllegalArgumentException("Invalid new list type", e);
    1.70 +        }
    1.71 +    }
    1.72 +
    1.73 +    private <T> void checkNotRegistered(final Map<String, List<T>> listenersMap,
    1.74 +                                        final String[] events) {
    1.75 +        synchronized (listenersMap) {
    1.76 +            for (final String event: events) {
    1.77 +                if (listenersMap.get(event) != null) {
    1.78 +                    throw new IllegalStateException(
    1.79 +                        "Already registered " + event + " under a different type");
    1.80 +                }
    1.81 +            }
    1.82 +        }
    1.83 +    }
    1.84 +
    1.85 +    private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
    1.86 +                                        final T listener,
    1.87 +                                        final String[] events) {
    1.88 +        synchronized (listenersMap) {
    1.89 +            for (final String event : events) {
    1.90 +                List<T> listeners = listenersMap.get(event);
    1.91 +                if ((listeners == null ||
    1.92 +                     !listeners.remove(listener)) && !AppConstants.RELEASE_BUILD) {
    1.93 +                    throw new IllegalArgumentException(event + " was not registered");
    1.94 +                }
    1.95 +            }
    1.96 +        }
    1.97 +    }
    1.98 +
    1.99 +    @SuppressWarnings("unchecked")
   1.100 +    public void registerGeckoThreadListener(final NativeEventListener listener,
   1.101 +                                            final String... events) {
   1.102 +        checkNotRegistered(mGeckoThreadJSONListeners, events);
   1.103 +
   1.104 +        // For listeners running on the Gecko thread, we want to notify the listeners
   1.105 +        // outside of our synchronized block, because the listeners may take an
   1.106 +        // indeterminate amount of time to run. Therefore, to ensure concurrency when
   1.107 +        // iterating the list outside of the synchronized block, we use a
   1.108 +        // CopyOnWriteArrayList.
   1.109 +        registerListener((Class)CopyOnWriteArrayList.class,
   1.110 +                         mGeckoThreadNativeListeners, listener, events);
   1.111 +    }
   1.112 +
   1.113 +    @Deprecated // Use NativeEventListener instead
   1.114 +    @SuppressWarnings("unchecked")
   1.115 +    private void registerGeckoThreadListener(final GeckoEventListener listener,
   1.116 +                                             final String... events) {
   1.117 +        checkNotRegistered(mGeckoThreadNativeListeners, events);
   1.118 +
   1.119 +        registerListener((Class)CopyOnWriteArrayList.class,
   1.120 +                         mGeckoThreadJSONListeners, listener, events);
   1.121 +    }
   1.122 +
   1.123 +    public void unregisterGeckoThreadListener(final NativeEventListener listener,
   1.124 +                                              final String... events) {
   1.125 +        unregisterListener(mGeckoThreadNativeListeners, listener, events);
   1.126 +    }
   1.127 +
   1.128 +    @Deprecated // Use NativeEventListener instead
   1.129 +    private void unregisterGeckoThreadListener(final GeckoEventListener listener,
   1.130 +                                               final String... events) {
   1.131 +        unregisterListener(mGeckoThreadJSONListeners, listener, events);
   1.132 +    }
   1.133 +
   1.134 +    @Deprecated // Use one of the variants above.
   1.135 +    public void registerEventListener(final String event, final GeckoEventListener listener) {
   1.136 +        registerGeckoThreadListener(listener, event);
   1.137 +    }
   1.138 +
   1.139 +    @Deprecated // Use one of the variants above
   1.140 +    public void unregisterEventListener(final String event, final GeckoEventListener listener) {
   1.141 +        unregisterGeckoThreadListener(listener, event);
   1.142 +    }
   1.143 +
   1.144 +    public void dispatchEvent(final NativeJSContainer message) {
   1.145 +        EventCallback callback = null;
   1.146 +        try {
   1.147 +            // First try native listeners.
   1.148 +            final String type = message.getString("type");
   1.149 +
   1.150 +            final List<NativeEventListener> listeners;
   1.151 +            synchronized (mGeckoThreadNativeListeners) {
   1.152 +                listeners = mGeckoThreadNativeListeners.get(type);
   1.153 +            }
   1.154 +
   1.155 +            final String guid = message.optString(GUID, null);
   1.156 +            if (guid != null) {
   1.157 +                callback = new GeckoEventCallback(guid, type);
   1.158 +            }
   1.159 +
   1.160 +            if (listeners != null) {
   1.161 +                if (listeners.size() == 0) {
   1.162 +                    Log.w(LOGTAG, "No listeners for " + type);
   1.163 +                }
   1.164 +                for (final NativeEventListener listener : listeners) {
   1.165 +                    listener.handleMessage(type, message, callback);
   1.166 +                }
   1.167 +                // If we found native listeners, we assume we don't have any JSON listeners
   1.168 +                // and return early. This assumption is checked when registering listeners.
   1.169 +                return;
   1.170 +            }
   1.171 +        } catch (final IllegalArgumentException e) {
   1.172 +            // Message doesn't have a "type" property, fallback to JSON
   1.173 +        }
   1.174 +        try {
   1.175 +            // If we didn't find native listeners, try JSON listeners.
   1.176 +            dispatchEvent(new JSONObject(message.toString()), callback);
   1.177 +        } catch (final JSONException e) {
   1.178 +            Log.e(LOGTAG, "Cannot parse JSON");
   1.179 +        } catch (final UnsupportedOperationException e) {
   1.180 +            Log.e(LOGTAG, "Cannot convert message to JSON");
   1.181 +        }
   1.182 +    }
   1.183 +
   1.184 +    public void dispatchEvent(final JSONObject message, final EventCallback callback) {
   1.185 +        // {
   1.186 +        //   "type": "value",
   1.187 +        //   "event_specific": "value",
   1.188 +        //   ...
   1.189 +        try {
   1.190 +            final String type = message.getString("type");
   1.191 +
   1.192 +            List<GeckoEventListener> listeners;
   1.193 +            synchronized (mGeckoThreadJSONListeners) {
   1.194 +                listeners = mGeckoThreadJSONListeners.get(type);
   1.195 +            }
   1.196 +            if (listeners == null || listeners.size() == 0) {
   1.197 +                Log.w(LOGTAG, "No listeners for " + type);
   1.198 +
   1.199 +                // If there are no listeners, cancel the callback to prevent Gecko-side observers
   1.200 +                // from being leaked.
   1.201 +                if (callback != null) {
   1.202 +                    callback.sendCancel();
   1.203 +                }
   1.204 +                return;
   1.205 +            }
   1.206 +            for (final GeckoEventListener listener : listeners) {
   1.207 +                listener.handleMessage(type, message);
   1.208 +            }
   1.209 +        } catch (final JSONException e) {
   1.210 +            Log.e(LOGTAG, "handleGeckoMessage throws " + e, e);
   1.211 +        }
   1.212 +    }
   1.213 +
   1.214 +    @Deprecated
   1.215 +    public static void sendResponse(JSONObject message, Object response) {
   1.216 +        sendResponseHelper(STATUS_SUCCESS, message, response);
   1.217 +    }
   1.218 +
   1.219 +    @Deprecated
   1.220 +    public static void sendError(JSONObject message, Object response) {
   1.221 +        sendResponseHelper(STATUS_ERROR, message, response);
   1.222 +    }
   1.223 +
   1.224 +    @Deprecated
   1.225 +    private static void sendResponseHelper(String status, JSONObject message, Object response) {
   1.226 +        try {
   1.227 +            final JSONObject wrapper = new JSONObject();
   1.228 +            wrapper.put(GUID, message.getString(GUID));
   1.229 +            wrapper.put("status", status);
   1.230 +            wrapper.put("response", response);
   1.231 +            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
   1.232 +                    message.getString("type") + ":Response", wrapper.toString()));
   1.233 +        } catch (final JSONException e) {
   1.234 +            Log.e(LOGTAG, "Unable to send response", e);
   1.235 +        }
   1.236 +    }
   1.237 +
   1.238 +    private static class GeckoEventCallback implements EventCallback {
   1.239 +        private final String guid;
   1.240 +        private final String type;
   1.241 +        private boolean sent;
   1.242 +
   1.243 +        public GeckoEventCallback(final String guid, final String type) {
   1.244 +            this.guid = guid;
   1.245 +            this.type = type;
   1.246 +        }
   1.247 +
   1.248 +        public void sendSuccess(final Object response) {
   1.249 +            sendResponse(STATUS_SUCCESS, response);
   1.250 +        }
   1.251 +
   1.252 +        public void sendError(final Object response) {
   1.253 +            sendResponse(STATUS_ERROR, response);
   1.254 +        }
   1.255 +
   1.256 +        public void sendCancel() {
   1.257 +            sendResponse(STATUS_CANCEL, null);
   1.258 +        }
   1.259 +
   1.260 +        private void sendResponse(final String status, final Object response) {
   1.261 +            if (sent) {
   1.262 +                throw new IllegalStateException("Callback has already been executed for type=" +
   1.263 +                        type + ", guid=" + guid);
   1.264 +            }
   1.265 +
   1.266 +            sent = true;
   1.267 +
   1.268 +            try {
   1.269 +                final JSONObject wrapper = new JSONObject();
   1.270 +                wrapper.put(GUID, guid);
   1.271 +                wrapper.put("status", status);
   1.272 +                wrapper.put("response", response);
   1.273 +                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(type + ":Response",
   1.274 +                        wrapper.toString()));
   1.275 +            } catch (final JSONException e) {
   1.276 +                Log.e(LOGTAG, "Unable to send response for: " + type, e);
   1.277 +            }
   1.278 +        }
   1.279 +    }
   1.280 +}

mercurial