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 +}