org.greenrobot.eventbus.EventBus.java Source code

Java tutorial

Introduction

Here is the source code for org.greenrobot.eventbus.EventBus.java

Source

/*
 * Copyright (C) 2012-2016 Markus Junginger, greenrobot (http://greenrobot.org)
 *
 * 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 org.greenrobot.eventbus;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;

import org.greenrobot.eventbus.annotation.Params;
import org.greenrobot.eventbus.annotation.Subscribe;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;

/**
 * EventBus is a central publish/subscribe event system for Android. Events are posted ({@link #post(String, JsonObject)}) to the
 * bus, which delivers it to subscribers that have a matching handler method for the event type. To receive events,
 * subscribers must register themselves to the bus using {@link #register(Object)}. Once registered, subscribers
 * receive events until {@link #unregister(Object)} is called. Event handling methods must be annotated by
 * {@link Subscribe}, must be public, return nothing (void), and have exactly one parameter
 * (the event).
 *
 * @author Markus Junginger, greenrobot
 */
public class EventBus {

    /**
     * Log tag, apps may override it.
     */
    public static String TAG = "EventBus";

    static volatile EventBus defaultInstance;

    private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();
    private static final Map<Class<?>, List<Class<?>>> eventTypesCache = new HashMap<>();

    private final List<Service> serviceList = new ArrayList<>();
    private final Map<String, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
    private final Map<Object, List<String>> typesBySubscriber;

    private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
        @Override
        protected PostingThreadState initialValue() {
            return new PostingThreadState();
        }
    };

    private final HandlerPoster mainThreadPoster;
    private final BackgroundPoster backgroundPoster;
    private final AsyncPoster asyncPoster;
    private final SubscriberMethodFinder subscriberMethodFinder;
    private final ExecutorService executorService;

    private final boolean logNoSubscriberMessages;

    /**
     * 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;
    }

    public static EventBusBuilder builder() {
        return new EventBusBuilder();
    }

    /**
     * For unit test primarily.
     */
    public static void clearCaches() {
        SubscriberMethodFinder.clearCaches();
        eventTypesCache.clear();
    }

    /**
     * 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() {
        this(DEFAULT_BUILDER);
    }

    EventBus(EventBusBuilder builder) {
        subscriptionsByEventType = new HashMap<>();
        typesBySubscriber = new HashMap<>();
        mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
        backgroundPoster = new BackgroundPoster(this);
        asyncPoster = new AsyncPoster(this);
        subscriberMethodFinder = new SubscriberMethodFinder();
        logNoSubscriberMessages = builder.logNoSubscriberMessages;
        executorService = builder.executorService;
    }

    /**
     * Registers the given subscriber to receive events. Subscribers must call {@link #unregister(Object)} once they
     * are no longer interested in receiving events.
     * <p/>
     * Subscribers have event handling methods that must be annotated by {@link Subscribe}.
     * The {@link Subscribe} annotation also allows configuration like {@link
     * ThreadMode} and priority.
     */
    public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

    /**
     * <p/>
     * register system service
     *
     * @param context
     * @param xmId
     */
    public void register(Context context, int xmId) {
        XmlResourceParser parser = context.getResources().getXml(xmId);
        try {
            Service service = null;
            Method method = null;
            Method.Data data = null;
            Page page = null;
            Page.Bundle bundle = null;

            int event = parser.getEventType();//
            while (event != XmlPullParser.END_DOCUMENT) {
                switch (event) {
                case XmlPullParser.START_TAG://??
                    if (parser.getName().equalsIgnoreCase("service")) {
                        service = new Service(parser.getAttributeValue(null, "url"),
                                Class.forName(parser.getAttributeValue(null, "class")));
                        if (service.getUrl() == null)
                            throw new EventBusException("Service[" + service + "] url not null");
                    } else if (parser.getName().equalsIgnoreCase("method")) {
                        method = new Method(parser.getAttributeValue(null, "id"),
                                parser.getAttributeValue(null, "name"));
                        if (method.getId() == null || method.getName() == null)
                            throw new EventBusException("Service.Method[" + method + "] id or name not null");
                        if (!isExistPublicStaticMethod(service.getClazz(), method.getName())) {
                            throw new EventBusException("Service.Method[" + method
                                    + "] not find public static method, the method name is " + method.getName());
                        }
                    } else if (parser.getName().equalsIgnoreCase("data")) {
                        data = new Method.Data(parser.getAttributeValue(null, "id"),
                                parser.getAttributeValue(null, "key"),
                                (Class<? extends Serializable>) Class
                                        .forName(parser.getAttributeValue(null, "type")),
                                parser.getAttributeBooleanValue(null, "isNull", true));
                        if (data.getId() == null || data.getKey() == null)
                            throw new EventBusException("Service.Method.Data[" + data + "] id or key not null");
                    } else if (parser.getName().equalsIgnoreCase("page")) {
                        page = new Page(parser.getAttributeValue(null, "id"),
                                parser.getAttributeIntValue(null, "requestCode", -1));
                        if (page.getId() == null)
                            throw new EventBusException("Service.Page[" + page + "] id  not null");
                    } else if (parser.getName().equalsIgnoreCase("bundle")) {
                        bundle = new Page.Bundle(parser.getAttributeValue(null, "id"),
                                parser.getAttributeValue(null, "key"),
                                (Class<? extends Serializable>) Class
                                        .forName(parser.getAttributeValue(null, "type")),
                                parser.getAttributeBooleanValue(null, "isNull", true));
                        if (bundle.getId() == null || bundle.getKey() == null)
                            throw new EventBusException("Service.Page.Bundle[" + bundle + "] id or key not null");
                    }
                    break;
                case XmlPullParser.END_TAG://???
                    if (parser.getName().equalsIgnoreCase("service")) {
                        if (serviceList.contains(service))
                            break;

                        if (findServiceByUrl(service.getUrl()) == null) {
                            serviceList.add(service);
                            service = null;
                        } else {
                            throw new EventBusException("Service[" + service + "] the url is already registered");
                        }
                    } else if (parser.getName().equalsIgnoreCase("method")) {
                        if (service.getMethods().contains(method))
                            break;

                        if (findMethodById(service, method.getId()) == null) {
                            service.getMethods().add(method);
                            method = null;
                        } else {
                            throw new EventBusException("Service.Method[" + method + "] the id already registered");
                        }
                    } else if (parser.getName().equalsIgnoreCase("data")) {
                        for (Method.Data item : method.getDataList()) {
                            if (item.getKey().equals(data.getKey()) || item.getId().equals(data.getId())) {
                                throw new EventBusException(
                                        "Service.Method.Data[" + data + "] the id or key already registered");
                            }
                        }

                        method.getDataList().add(data);
                        data = null;
                    } else if (parser.getName().equalsIgnoreCase("page")) {
                        if (service.getPages().contains(page))
                            break;

                        if (findPageById(service, page.getId()) == null) {
                            service.getPages().add(page);
                            page = null;
                        } else {
                            throw new EventBusException("Service.Page[" + page + "] the id already registered");
                        }
                    } else if (parser.getName().equalsIgnoreCase("bundle")) {
                        for (Page.Bundle item : page.getBundleList()) {
                            if (item.getKey().equals(bundle.getKey()) || item.getId().equals(bundle.getId())) {
                                throw new EventBusException(
                                        "Service.Page.Bundle[" + bundle + "] the id or key already registered");
                            }
                        }

                        page.getBundleList().add(bundle);
                        bundle = null;
                    }
                    break;
                }
                event = parser.next();//
            }
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            throw new EventBusException(e);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Log.d(TAG, "Length:" + serviceList.size() + " Service" + serviceList.toString());
    }

    private boolean isExistPublicStaticMethod(Class<?> clazz, String name) throws EventBusException {
        if (clazz != null && !clazz.isAnnotationPresent(org.greenrobot.eventbus.annotation.Service.class)) {
            throw new EventBusException("Class[" + clazz.getName() + "] not add Service Annotation");
        }
        try {
            while (clazz != null) {
                java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();
                for (java.lang.reflect.Method item : methods) {
                    if (name.equals(item.getName())) {
                        int modifiers = item.getModifiers();
                        if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & Modifier.STATIC) != 0) {
                            return true;
                        }
                    }
                }
                clazz = clazz.getSuperclass();
            }
        } catch (Exception e) {
            Log.w(TAG, e.getMessage());
        }
        return false;
    }

    // Must be called in synchronized block
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        String eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException(
                        "Subscriber " + subscriber.getClass() + " already registered to event " + eventType);
            }
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        List<String> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);
    }

    public synchronized boolean isRegistered(Object subscriber) {
        return typesBySubscriber.containsKey(subscriber);
    }

    /**
     * Only updates subscriptionsByEventType, not typesBySubscriber! Caller must update typesBySubscriber.
     */
    private void unSubscribeByEventType(Object subscriber, String eventType) {
        List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions != null) {
            int size = subscriptions.size();
            for (int i = 0; i < size; i++) {
                Subscription subscription = subscriptions.get(i);
                if (subscription.subscriber == subscriber) {
                    subscription.active = false;
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

    /**
     * Unregisters the given subscriber from all event classes.
     */
    public synchronized void unregister(Object subscriber) {
        List<String> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            for (String eventType : subscribedTypes) {
                unSubscribeByEventType(subscriber, eventType);
            }
            typesBySubscriber.remove(subscriber);
        } else {
            Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

    /**
     * Posts the given event to the event bus.
     */
    public void post(String url, JsonObject event) {
        PostingThreadState postingState = currentPostingThreadState.get();
        List<PostEvent> eventQueue = postingState.eventQueue;
        eventQueue.add(new PostEvent(url, event));

        if (!postingState.isPosting) {
            postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
            postingState.isPosting = true;
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
                while (!eventQueue.isEmpty()) {
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }
    }

    /**
     * call the register service by the event bus.
     */
    public void call(Context context, EUrl url, JsonObject jsonObject, OnMethodCallBack callBack) {
        try {
            Service service = findServiceByUrl(url.getUrl());
            if (service == null)
                throw new EventBusException(
                        "EventBus Call Method, but not find Service by url[" + url.getUrl() + "]");
            Method method = findMethodById(service, url.getId());
            if (method == null) {
                throw new EventBusException("EventBus Call Method, but not find Service.Method by id[" + url.getId()
                        + "] from " + service);
            }
            String mName = method.getName();
            Class<?> clazz = service.getClazz();
            List<Method.Data> dataList = method.getDataList();

            List<Object> params = new ArrayList<>();
            java.lang.reflect.Method execMethod = findExecMethod(clazz, mName, context, callBack, jsonObject,
                    dataList, params);
            if (execMethod == null) {
                throw new EventBusException("EventBus Call Method, but not find ExecMethod by name and paramList["
                        + method.getName() + "] from " + service);
            }

            switch (params.size()) {
            case 0:
                execMethod.invoke(null);
                break;
            case 1:
                execMethod.invoke(null, params.get(0));
                break;
            case 2:
                execMethod.invoke(null, params.get(0), params.get(1));
                break;
            case 3:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2));
                break;
            case 4:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3));
                break;
            case 5:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3), params.get(4));
                break;
            case 6:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3), params.get(4),
                        params.get(5));
                break;
            case 7:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3), params.get(4),
                        params.get(5), params.get(6));
                break;
            case 8:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3), params.get(4),
                        params.get(5), params.get(6), params.get(7));
                break;
            case 9:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3), params.get(4),
                        params.get(5), params.get(6), params.get(7), params.get(8));
                break;
            case 10:
                execMethod.invoke(null, params.get(0), params.get(1), params.get(2), params.get(3), params.get(4),
                        params.get(5), params.get(6), params.get(7), params.get(8), params.get(10));
                break;
            default:
                throw new EventBusException("we will very sorry, the method has more 10 params, but you translate "
                        + params.size() + " params for " + execMethod.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void call(EUrl url, JsonObject params) {
        this.call(null, url, params, null);
    }

    public void call(EUrl url, JsonObject params, OnMethodCallBack callBack) {
        this.call(null, url, params, callBack);
    }

    public void call(Context context, EUrl url, JsonObject params) {
        this.call(context, url, params, null);
    }

    public void open(Context context, EUrl url, JsonObject jsonObject) {
        try {
            Service service = findServiceByUrl(url.getUrl());
            if (service == null)
                throw new EventBusException(
                        "EventBus Open Page, but  not find Service by url[" + url.getUrl() + "]");
            Page page = findPageById(service, url.getId());
            if (page == null) {
                throw new EventBusException(
                        "EventBus Open Page, but not find Service.Page by id[" + url.getId() + "] from " + service);
            }
            if (jsonObject == null) {
                for (Page.Bundle item : page.getBundleList()) {
                    if (item.getNull() == false) {
                        throw new EventBusException("Page[" + page + "], the id[" + item.getId()
                                + "] has null value to give " + item.getKey());
                    }
                }
            }

            Intent intent = new Intent(context, service.getClazz());
            Bundle bundle = getBundleByPage(page, jsonObject);
            if (bundle != null) {
                intent.putExtras(bundle);
            }
            if (context instanceof Activity && page.getRequestCode() > 0) {
                ((Activity) context).startActivityForResult(intent, page.getRequestCode());
            } else {
                context.startActivity(intent);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private java.lang.reflect.Method findExecMethod(Class<?> clazz, String name, Context context,
            OnMethodCallBack callBack, JsonObject json, List<Method.Data> dataList, List<Object> outParams) {
        List<java.lang.reflect.Method> like = null;
        while (clazz != null) {
            java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();
            for (java.lang.reflect.Method method : methods) {
                if (method.getName().equals(name)) {//???
                    outParams.clear();
                    int paramsCount = 0;//?
                    boolean isSuccess = true;//???
                    //?Params??ContextOnMethodCallBack
                    Annotation[][] annotations = method.getParameterAnnotations();
                    Class<?>[] params = method.getParameterTypes();
                    for (int i = 0; annotations != null && i < annotations.length; i++) {
                        Params p = getParamsAnnotation(annotations[i]);
                        if (p == null) {
                            if (Context.class.isAssignableFrom(params[i])) {
                                outParams.add(context);
                            } else if (OnMethodCallBack.class.isAssignableFrom(params[i])) {
                                outParams.add(callBack);
                            } else {
                                isSuccess = false;
                                break;
                            }
                        } else {
                            boolean add = false;
                            for (Method.Data data : dataList) {
                                if (p.value().equalsIgnoreCase(data.getKey())) {
                                    add = true;
                                    paramsCount++;
                                    outParams.add(getValue(data, json));
                                    break;
                                }
                            }
                            if (!add) {
                                isSuccess = false;
                                break;
                            }
                        }
                    }
                    //Params??xml?
                    if (isSuccess) {
                        if (paramsCount == dataList.size()) {
                            return method;
                        }
                        like = new ArrayList<>();
                        like.add(method);
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
        if (like != null) {
            throw new EventBusException(
                    "EventBus Call Method, but not find ExecMethod because method params count and xml define params count is not sameplease modify under method "
                            + like);
        }
        return null;
    }

    private Params getParamsAnnotation(Annotation[] as) {
        for (int i = 0; as != null && i < as.length; i++) {
            if (as[i] instanceof Params) {
                return (Params) as[i];
            }
        }
        return null;
    }

    private Object getValue(Method.Data data, JsonObject json) {
        if (data == null)
            return null;

        String id = data.getId();
        Class<?> clazz = data.getType();
        boolean isNull = data.getNull();

        JsonElement value = json == null ? new JsonNull() : json.get(id);
        if (!isNull && value.isJsonNull())
            throw new EventBusException(
                    "Method.Data[" + data + "], the id[" + id + "] has null value to give " + data.getType());

        if (!value.isJsonNull()) {
            if (clazz == Boolean.class) {
                return value.getAsBoolean();
            } else if (clazz == Integer.class) {
                return value.getAsInt();
            } else if (clazz == Long.class) {
                return value.getAsLong();
            } else if (clazz == Float.class) {
                return value.getAsFloat();
            } else if (clazz == Double.class) {
                return value.getAsDouble();
            } else if (clazz == String.class) {
                return value.getAsString();
            } else if (clazz == Byte.class) {
                return value.getAsByte();
            } else if (clazz == char.class) {
                return value.getAsCharacter();
            } else {
                return new Gson().fromJson(value, clazz);
            }
        }
        return null;
    }

    private Bundle getBundleByPage(Page page, JsonObject json) throws Exception {
        if (page != null && json != null && page.getBundleList().size() > 0) {
            Bundle bundle = new Bundle();
            for (Page.Bundle item : page.getBundleList()) {
                String id = item.getId();
                String key = item.getKey();
                Class<? extends Serializable> clazz = item.getType();
                boolean isNull = item.getNull();

                JsonElement value = json.get(id);
                if (!isNull && value.isJsonNull())
                    throw new EventBusException(
                            "Page.Bundle[" + item + "], the id[" + id + "] has null value to give " + key);

                if (!value.isJsonNull()) {
                    if (clazz == Boolean.class) {
                        bundle.putBoolean(key, value.getAsBoolean());
                    } else if (clazz == Integer.class) {
                        bundle.putInt(key, value.getAsInt());
                    } else if (clazz == Long.class) {
                        bundle.putLong(key, value.getAsLong());
                    } else if (clazz == Float.class) {
                        bundle.putFloat(key, value.getAsFloat());
                    } else if (clazz == Double.class) {
                        bundle.putDouble(key, value.getAsDouble());
                    } else if (clazz == String.class) {
                        bundle.putString(key, value.getAsString());
                    } else if (clazz == Byte.class) {
                        bundle.putByte(key, value.getAsByte());
                    } else if (clazz == char.class) {
                        bundle.putChar(key, value.getAsCharacter());
                    } else {
                        bundle.putSerializable(key, new Gson().fromJson(value, clazz));
                    }
                }
            }
            return bundle;
        }
        return null;
    }

    public boolean hasSubscriberForEvent(String url) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            subscriptions = subscriptionsByEventType.get(url);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            return true;
        }
        return false;
    }

    private Service findServiceByUrl(String url) {
        if (url != null) {
            for (Service item : serviceList) {
                if (url.equalsIgnoreCase(item.getUrl())) {
                    return item;
                }
            }
        }
        return null;
    }

    private Method findMethodById(Service service, String id) {
        if (service != null && id != null) {
            for (Method item : service.getMethods()) {
                if (id.equalsIgnoreCase(item.getId())) {
                    return item;
                }
            }
        }
        return null;
    }

    private Page findPageById(Service service, String id) {
        if (service != null && id != null) {
            for (Page item : service.getPages()) {
                if (id.equalsIgnoreCase(item.getId())) {
                    return item;
                }
            }
        }
        return null;
    }

    private void postSingleEvent(PostEvent post, PostingThreadState postingState) throws Error {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            subscriptions = subscriptionsByEventType.get(post.url);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            for (Subscription subscription : subscriptions) {
                postingState.event = post;
                postingState.subscription = subscription;
                boolean aborted = false;
                try {
                    postToSubscription(subscription, post, postingState.isMainThread);
                    aborted = postingState.canceled;
                } finally {
                    postingState.event = null;
                    postingState.subscription = null;
                    postingState.canceled = false;
                }
                if (aborted) {
                    break;
                }
            }
        } else {
            if (logNoSubscriberMessages) {
                Log.d(TAG, "No subscribers registered for event " + post.url);
            }
        }
    }

    private void postToSubscription(Subscription subscription, PostEvent post, boolean isMainThread) {
        switch (subscription.subscriberMethod.threadMode) {
        case POSTING:
            invokeSubscriber(subscription, post.event);
            break;
        case MAIN:
            if (isMainThread) {
                invokeSubscriber(subscription, post.event);
            } else {
                mainThreadPoster.enqueue(subscription, post.event);
            }
            break;
        case BACKGROUND:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, post.event);
            } else {
                invokeSubscriber(subscription, post.event);
            }
            break;
        case ASYNC:
            asyncPoster.enqueue(subscription, post.event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
        }
    }

    /**
     * Invokes the subscriber if the subscriptions is still active. Skipping subscriptions prevents race conditions
     * between {@link #unregister(Object)} and event delivery. Otherwise the event might be delivered after the
     * subscriber unregistered. This is particularly important for main thread delivery and registrations bound to the
     * live cycle of an Activity or Fragment.
     */
    void invokeSubscriber(PendingPost pendingPost) {
        Object event = pendingPost.event;
        Subscription subscription = pendingPost.subscription;
        PendingPost.releasePendingPost(pendingPost);
        if (subscription.active) {
            invokeSubscriber(subscription, (JsonObject) event);
        }
    }

    void invokeSubscriber(Subscription subscription, JsonObject event) {
        try {
            subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
        } catch (InvocationTargetException e) {
            Log.e("exception", e.getMessage());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception", e);
        }
    }

    /**
     * For ThreadLocal, much faster to set (and get multiple values).
     */
    final static class PostingThreadState {
        final List<PostEvent> eventQueue = new ArrayList<>();
        boolean isPosting;
        boolean isMainThread;
        Subscription subscription;
        PostEvent event;
        boolean canceled;
    }

    final static class PostEvent {
        String url;
        JsonObject event;

        public PostEvent(String url, JsonObject event) {
            this.url = url;
            this.event = event;
        }
    }

    ExecutorService getExecutorService() {
        return executorService;
    }
}