/* * Copyright (C) 2012 Markus Junginger, greenrobot (http://greenrobot.de) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.external.eventbus; import android.os.Looper; import android.util.Log; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * EventBus is a central publish/subscribe event system for Android. Events are posted ({@link #post(Object)} to the * bus, which delivers it to subscribers that have matching handler methods for the event type. To receive events, * subscribers must register themselves to the bus using the {@link #register(Object)} method. Once registered, * subscribers receive events until the call of {@link #unregister(Object)}. By default, subscribers will handle events * in methods named "onEvent". * * @author Markus Junginger, greenrobot */ public class EventBus { static ExecutorService executorService = Executors.newCachedThreadPool(); /** Log tag, apps may override it. */ public static String TAG = "Event"; private static volatile EventBus defaultInstance; private static final Map<Class<?>, List<Class<?>>> eventTypesCache = new HashMap<Class<?>, List<Class<?>>>(); private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType; private final Map<Object, List<Class<?>>> typesBySubscriber; private final Map<Class<?>, Object> stickyEvents; private final ThreadLocal<List<Object>> currentThreadEventQueue = new ThreadLocal<List<Object>>() { @Override protected List<Object> initialValue() { return new ArrayList<Object>(); } }; private final ThreadLocal<BooleanWrapper> currentThreadIsPosting = new ThreadLocal<BooleanWrapper>() { @Override protected BooleanWrapper initialValue() { return new BooleanWrapper(); } }; private String defaultMethodName = "onEvent"; private final HandlerPoster mainThreadPoster; private final BackgroundPoster backgroundPoster; private final AsyncPoster asyncPoster; private final SubscriberMethodFinder subscriberMethodFinder; private boolean subscribed; private boolean logSubscriberExceptions; /** Convenience singleton for apps using a process-wide EventBus instance. */ public static EventBus getDefault() { if (defaultInstance == null) { synchronized (EventBus.class) { if (defaultInstance == null) { defaultInstance = new EventBus(); } } } return defaultInstance; } /** For unit test primarily. */ public static void clearCaches() { SubscriberMethodFinder.clearCaches(); eventTypesCache.clear(); } /** * Method name verification is done for methods starting with onEvent to avoid typos; using this method you can * exclude subscriber classes from this check. */ public static void skipMethodNameVerificationFor(Class<?> clazz) { SubscriberMethodFinder.skipMethodNameVerificationFor(clazz); } /** For unit test primarily. */ public static void clearSkipMethodNameVerifications() { SubscriberMethodFinder.clearSkipMethodNameVerifications(); } /** * Creates a new EventBus instance; each instance is a separate scope in which events are delivered. To use a * central bus, consider {@link #getDefault()}. */ public EventBus() { subscriptionsByEventType = new HashMap<Class<?>, CopyOnWriteArrayList<Subscription>>(); typesBySubscriber = new HashMap<Object, List<Class<?>>>(); stickyEvents = new ConcurrentHashMap<Class<?>, Object>(); mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10); backgroundPoster = new BackgroundPoster(this); asyncPoster = new AsyncPoster(this); subscriberMethodFinder = new SubscriberMethodFinder(); logSubscriberExceptions = true; } /** * Before registering any subscribers, use this method to configure if EventBus should log exceptions thrown by * subscribers (default: true). */ public void configureLogSubscriberExceptions(boolean logSubscriberExceptions) { if (subscribed) { throw new EventBusException("This method must be called before any registration"); } this.logSubscriberExceptions = logSubscriberExceptions; } /** * Registers the given subscriber to receive events. Subscribers must call {@link #unregister(Object)} once they are * no longer interested in receiving events. * * Subscribers have event handling methods that are identified by their name, typically called "onEvent". Event * handling methods must have exactly one parameter, the event. If the event handling method is to be called in a * specific thread, a modifier is appended to the method name. Valid modifiers match one of the {@link ThreadMode} * enums. For example, if a method is to be called in the UI/main thread by EventBus, it would be called * "onEventMainThread". */ public void register(Object subscriber) { register(subscriber, defaultMethodName, false); } /** * Like {@link #register(Object)}, but allows to define a custom method name for event handler methods. */ public void register(Object subscriber, String methodName) { register(subscriber, methodName, false); } /** * Like {@link #register(Object)}, but also triggers delivery of the most recent sticky event (posted with * {@link #postSticky(Object)}) to the given subscriber. */ public void registerSticky(Object subscriber) { register(subscriber, defaultMethodName, true); } /** * Like {@link #registerSticky(Object)}, but allows to define a custom method name for event handler methods. */ public void registerSticky(Object subscriber, String methodName) { register(subscriber, methodName, true); } private void register(Object subscriber, String methodName, boolean sticky) { List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass(), methodName); for (SubscriberMethod subscriberMethod : subscriberMethods) { subscribe(subscriber, subscriberMethod, sticky); } } /** * Like {@link #register(Object)}, but only registers the subscriber for the given event types. */ public void register(Object subscriber, Class<?> eventType, Class<?>... moreEventTypes) { register(subscriber, defaultMethodName, false, eventType, moreEventTypes); } /** * Like {@link #register(Object, String)}, but only registers the subscriber for the given event types. */ public synchronized void register(Object subscriber, String methodName, Class<?> eventType, Class<?>... moreEventTypes) { register(subscriber, methodName, false, eventType, moreEventTypes); } /** * Like {@link #registerSticky(Object)}, but only registers the subscriber for the given event types. */ public void registerSticky(Object subscriber, Class<?> eventType, Class<?>... moreEventTypes) { register(subscriber, defaultMethodName, true, eventType, moreEventTypes); } /** * Like {@link #registerSticky(Object, String)}, but only registers the subscriber for the given event types. */ public synchronized void registerSticky(Object subscriber, String methodName, Class<?> eventType, Class<?>... moreEventTypes) { register(subscriber, methodName, true, eventType, moreEventTypes); } private synchronized void register(Object subscriber, String methodName, boolean sticky, Class<?> eventType, Class<?>... moreEventTypes) { Class<?> subscriberClass = subscriber.getClass(); List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass, methodName); for (SubscriberMethod subscriberMethod : subscriberMethods) { if (eventType == subscriberMethod.eventType) { subscribe(subscriber, subscriberMethod, sticky); } else if (moreEventTypes != null) { for (Class<?> eventType2 : moreEventTypes) { if (eventType2 == subscriberMethod.eventType) { subscribe(subscriber, subscriberMethod, sticky); break; } } } } } private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky) { subscribed = true; Class<?> eventType = subscriberMethod.eventType; CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType); Subscription newSubscription = new Subscription(subscriber, subscriberMethod); if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<Subscription>(); subscriptionsByEventType.put(eventType, subscriptions); } else { for (Subscription subscription : subscriptions) { if (subscription.equals(newSubscription)) { throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event " + eventType); } } } subscriberMethod.method.setAccessible(true); subscriptions.add(newSubscription); List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber); if (subscribedEvents == null) { subscribedEvents = new ArrayList<Class<?>>(); typesBySubscriber.put(subscriber, subscribedEvents); } subscribedEvents.add(eventType); if (sticky) { Object stickyEvent; synchronized (stickyEvents) { stickyEvent = stickyEvents.get(eventType); } if (stickyEvent != null) { postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper()); } } } /** Unregisters the given subscriber for the given event classes. */ public synchronized void unregister(Object subscriber, Class<?>... eventTypes) { if (eventTypes.length == 0) { throw new IllegalArgumentException("Provide at least one event class"); } List<Class<?>> subscribedClasses = typesBySubscriber.get(subscriber); if (subscribedClasses != null) { for (Class<?> eventType : eventTypes) { unubscribeByEventType(subscriber, eventType); subscribedClasses.remove(eventType); } if (subscribedClasses.isEmpty()) { typesBySubscriber.remove(subscriber); } } else { Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass()); } } /** Only updates subscriptionsByEventType, not typesBySubscriber! Caller must update typesBySubscriber. */ private void unubscribeByEventType(Object subscriber, Class<?> eventType) { List<Subscription> subscriptions = subscriptionsByEventType.get(eventType); if (subscriptions != null) { int size = subscriptions.size(); for (int i = 0; i < size; i++) { if (subscriptions.get(i).subscriber == subscriber) { subscriptions.remove(i); i--; size--; } } } } /** Unregisters the given subscriber from all event classes. */ public synchronized void unregister(Object subscriber) { List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber); if (subscribedTypes != null) { for (Class<?> eventType : subscribedTypes) { unubscribeByEventType(subscriber, eventType); } typesBySubscriber.remove(subscriber); } else { Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass()); } } public synchronized boolean isregister(Object subscriber) { List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber); if(subscribedTypes != null) { return true; } else { return false; } } /** Posts the given event to the event bus. */ public void post(Object event) { List<Object> eventQueue = currentThreadEventQueue.get(); eventQueue.add(event); BooleanWrapper isPosting = currentThreadIsPosting.get(); if (isPosting.value) { return; } else { boolean isMainThread = Looper.getMainLooper() == Looper.myLooper(); isPosting.value = true; try { while (!eventQueue.isEmpty()) { postSingleEvent(eventQueue.remove(0), isMainThread); } } finally { isPosting.value = false; } } } /** * Posts the given event to the event bus and holds on to the event (because it is sticky). The most recent sticky * event of an event's type is kept in memory for future access. This can be {@link #registerSticky(Object)} or * {@link #getStickyEvent(Class)}. */ public void postSticky(Object event) { synchronized (stickyEvents) { stickyEvents.put(event.getClass(), event); } // Should be posted after it is putted, in case the subscriber wants to remove immediately post(event); } /** * Gets the most recent sticky event for the given type. * * @see #postSticky(Object) */ public Object getStickyEvent(Class<?> eventType) { synchronized (stickyEvents) { return stickyEvents.get(eventType); } } /** * Remove and gets the recent sticky event for the given type. * * @see #postSticky(Object) */ public Object removeStickyEvent(Class<?> eventType) { synchronized (stickyEvents) { return stickyEvents.remove(eventType); } } /** * Removes the sticky event if it equals to the given event. * * @return true if the events matched and the sticky event was removed. */ public boolean removeStickyEvent(Object event) { synchronized (stickyEvents) { Class<? extends Object> eventType = event.getClass(); Object existingEvent = stickyEvents.get(eventType); if (event.equals(existingEvent)) { stickyEvents.remove(eventType); return true; } else { return false; } } } private void postSingleEvent(Object event, boolean isMainThread) throws Error { Class<? extends Object> eventClass = event.getClass(); List<Class<?>> eventTypes = findEventTypes(eventClass); boolean subscriptionFound = false; int countTypes = eventTypes.size(); for (int h = 0; h < countTypes; h++) { Class<?> clazz = eventTypes.get(h); CopyOnWriteArrayList<Subscription> subscriptions; synchronized (this) { subscriptions = subscriptionsByEventType.get(clazz); } if (subscriptions != null) { for (Subscription subscription : subscriptions) { postToSubscription(subscription, event, isMainThread); } subscriptionFound = true; } } if (!subscriptionFound) { Log.d(TAG, "No subscripers registered for event " + eventClass); if (eventClass != NoSubscriberEvent.class && eventClass != SubscriberExceptionEvent.class) { post(new NoSubscriberEvent(this, event)); } } } private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { switch (subscription.subscriberMethod.threadMode) { case PostThread: invokeSubscriber(subscription, event); break; case MainThread: if (isMainThread) { invokeSubscriber(subscription, event); } else { mainThreadPoster.enqueue(subscription, event); } break; case BackgroundThread: if (isMainThread) { backgroundPoster.enqueue(subscription, event); } else { invokeSubscriber(subscription, event); } break; case Async: asyncPoster.enqueue(subscription, event); break; default: throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode); } } /** Finds all Class objects including super classes and interfaces. */ private List<Class<?>> findEventTypes(Class<?> eventClass) { synchronized (eventTypesCache) { List<Class<?>> eventTypes = eventTypesCache.get(eventClass); if (eventTypes == null) { eventTypes = new ArrayList<Class<?>>(); Class<?> clazz = eventClass; while (clazz != null) { eventTypes.add(clazz); addInterfaces(eventTypes, clazz.getInterfaces()); clazz = clazz.getSuperclass(); } eventTypesCache.put(eventClass, eventTypes); } return eventTypes; } } /** Recurses through super interfaces. */ static void addInterfaces(List<Class<?>> eventTypes, Class<?>[] interfaces) { for (Class<?> interfaceClass : interfaces) { if (!eventTypes.contains(interfaceClass)) { eventTypes.add(interfaceClass); addInterfaces(eventTypes, interfaceClass.getInterfaces()); } } } void invokeSubscriber(PendingPost pendingPost) { Object event = pendingPost.event; Subscription subscription = pendingPost.subscription; PendingPost.releasePendingPost(pendingPost); invokeSubscriber(subscription, event); } void invokeSubscriber(Subscription subscription, Object event) throws Error { try { subscription.subscriberMethod.method.invoke(subscription.subscriber, event); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (event instanceof SubscriberExceptionEvent) { // Don't send another SubscriberExceptionEvent to avoid infinite event recursion, just log Log.e(TAG, "SubscriberExceptionEvent subscriber " + subscription.subscriber.getClass() + " threw an exception", cause); SubscriberExceptionEvent exEvent = (SubscriberExceptionEvent) event; Log.e(TAG, "Initial event " + exEvent.causingEvent + " caused exception in " + exEvent.causingSubscriber, exEvent.throwable); } else { if (logSubscriberExceptions) { Log.e(TAG, "Could not dispatch event: " + event.getClass() + " to subscribing class " + subscription.subscriber.getClass(), cause); } SubscriberExceptionEvent exEvent = new SubscriberExceptionEvent(this, cause, event, subscription.subscriber); post(exEvent); } } catch (IllegalAccessException e) { throw new IllegalStateException("Unexpected exception", e); } } /** For ThreadLocal, much faster to set than storing a new Boolean. */ final static class BooleanWrapper { boolean value; } // Just an idea: we could provide a callback to post() to be notified, an alternative would be events, of course... /* public */interface PostCallback { void onPostCompleted(List<SubscriberExceptionEvent> exceptionEvents); } }