All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.pubnub.api.managers.SubscriptionManager Maven / Gradle / Ivy

Go to download

PubNub is a cross-platform client-to-client (1:1 and 1:many) push service in the cloud, capable of broadcasting real-time messages to millions of web and mobile clients simultaneously, in less than a quarter second!

There is a newer version: 4.6.5
Show newest version
package com.pubnub.api.managers;

import com.pubnub.api.PubNub;
import com.pubnub.api.builder.dto.StateOperation;
import com.pubnub.api.builder.dto.SubscribeOperation;
import com.pubnub.api.builder.dto.UnsubscribeOperation;
import com.pubnub.api.callbacks.PNCallback;
import com.pubnub.api.callbacks.ReconnectionCallback;
import com.pubnub.api.callbacks.SubscribeCallback;
import com.pubnub.api.endpoints.presence.Heartbeat;
import com.pubnub.api.endpoints.presence.Leave;
import com.pubnub.api.endpoints.pubsub.Subscribe;
import com.pubnub.api.enums.PNHeartbeatNotificationOptions;
import com.pubnub.api.enums.PNStatusCategory;
import com.pubnub.api.models.consumer.PNStatus;
import com.pubnub.api.models.server.SubscribeEnvelope;
import com.pubnub.api.models.server.SubscribeMessage;
import com.pubnub.api.workers.SubscribeMessageWorker;
import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingQueue;

@Slf4j
public class SubscriptionManager {

    private static final int HEARTBEAT_INTERVAL_MULTIPLIER = 1000;

    private PubNub pubnub;
    private Subscribe subscribeCall;
    private Heartbeat heartbeatCall;

    private LinkedBlockingQueue messageQueue;

    /**
     * Store the latest timetoken to subscribe with, null by default to get the latest timetoken.
     */
    private Long timetoken;

    /**
     * Keep track of Region to support PSV2 specification.
     */
    private String region;

    /**
     * Timer for heartbeat operations.
     */
    private Timer timer;

    private StateManager subscriptionState;
    private ListenerManager listenerManager;
    private ReconnectionManager reconnectionManager;
    private RetrofitManager retrofitManager;

    private Thread consumerThread;

    /**
     * lever to indicate if an announcement to the user about the subscription should be made.
     * the announcement happens only after the channel mix has been changed.
     */
    private boolean subscriptionStatusAnnounced;

    public SubscriptionManager(PubNub pubnubInstance, RetrofitManager retrofitManagerInstance) {
        this.pubnub = pubnubInstance;

        this.subscriptionStatusAnnounced = false;
        this.messageQueue = new LinkedBlockingQueue<>();
        this.subscriptionState = new StateManager();

        this.listenerManager = new ListenerManager(this.pubnub);
        this.reconnectionManager = new ReconnectionManager(this.pubnub);
        this.retrofitManager = retrofitManagerInstance;

        this.timetoken = 0L;

        this.reconnectionManager.setReconnectionListener(new ReconnectionCallback() {
            @Override
            public void onReconnection() {
                reconnect();
                PNStatus pnStatus = PNStatus.builder()
                        .error(false)
                        .category(PNStatusCategory.PNReconnectedCategory)
                        .build();

                subscriptionStatusAnnounced = true;
                listenerManager.announce(pnStatus);
            }
        });

        consumerThread = new Thread(new SubscribeMessageWorker(this.pubnub, listenerManager, messageQueue));
        consumerThread.start();
    }

    public final void addListener(SubscribeCallback listener) {
        listenerManager.addListener(listener);
    }

    public final void removeListener(SubscribeCallback listener) {
        listenerManager.removeListener(listener);
    }


    public final synchronized void reconnect() {
        this.startSubscribeLoop();
        this.registerHeartbeatTimer();
    }

    public final synchronized void disconnect() {
        stopHeartbeatTimer();
        stopSubscribeLoop();
    }


    public synchronized void stop() {
        disconnect();
        consumerThread.interrupt();
    }

    public final synchronized void adaptStateBuilder(final StateOperation stateOperation) {
        this.subscriptionState.adaptStateBuilder(stateOperation);
        reconnect();
    }

    public final synchronized void adaptSubscribeBuilder(final SubscribeOperation subscribeOperation) {
        this.subscriptionState.adaptSubscribeBuilder(subscribeOperation);
        // the channel mix changed, on the successful subscribe, there is going to be announcement.
        this.subscriptionStatusAnnounced = false;

        if (subscribeOperation.getTimetoken() != null) {
            this.timetoken = subscribeOperation.getTimetoken();
        }

        reconnect();
    }

    public final synchronized void adaptUnsubscribeBuilder(final UnsubscribeOperation unsubscribeOperation) {
        this.subscriptionState.adaptUnsubscribeBuilder(unsubscribeOperation);

        new Leave(pubnub, this.retrofitManager.getTransactionInstance())
            .channels(unsubscribeOperation.getChannels()).channelGroups(unsubscribeOperation.getChannelGroups())
            .async(new PNCallback() {
                @Override
                public void onResponse(final Boolean result, final PNStatus status) {
                    listenerManager.announce(status);
                }
        });

        // if we unsubscribed from all the channels, reset the timetoken back to zero and remove the region.
        if (this.subscriptionState.isEmpty()) {
            region = null;
            timetoken = 0L;
        }

        reconnect();
    }

    private void registerHeartbeatTimer() {
        // make sure only one timer is running at a time.
        stopHeartbeatTimer();

        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                performHeartbeatLoop();
            }
        }, 0, pubnub.getConfiguration().getHeartbeatInterval() * HEARTBEAT_INTERVAL_MULTIPLIER);

    }

    private void stopHeartbeatTimer() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    private void startSubscribeLoop() {
        // this function can be called from different points, make sure any old loop is closed
        stopSubscribeLoop();

        List combinedChannels = this.subscriptionState.prepareChannelList(true);
        List combinedChannelGroups = this.subscriptionState.prepareChannelGroupList(true);

        // do not start the subscribe loop if we have no channels to subscribe to.
        if (combinedChannels.isEmpty() && combinedChannelGroups.isEmpty()) {
            return;
        }

        subscribeCall = new Subscribe(pubnub, this.retrofitManager.getSubscriptionInstance())
                .channels(combinedChannels).channelGroups(combinedChannelGroups)
                .timetoken(timetoken).region(region)
                .filterExpression(pubnub.getConfiguration().getFilterExpression());

        subscribeCall.async(new PNCallback() {
            @Override
            public void onResponse(final SubscribeEnvelope result, final PNStatus status) {
                if (status.isError()) {

                    if (status.getCategory() == PNStatusCategory.PNTimeoutCategory) {
                        startSubscribeLoop();
                    } else {
                        disconnect();
                        listenerManager.announce(status);

                        // stop all announcements and ask the reconnection manager to start polling for connection restoration..
                        reconnectionManager.startPolling();
                    }

                    return;
                }

                if (!subscriptionStatusAnnounced) {
                    PNStatus pnStatus = PNStatus.builder()
                            .error(false)
                            .category(PNStatusCategory.PNConnectedCategory)
                            .statusCode(status.getStatusCode())
                            .authKey(status.getAuthKey())
                            .operation(status.getOperation())
                            .clientRequest(status.getClientRequest())
                            .origin(status.getOrigin())
                            .tlsEnabled(status.isTlsEnabled())
                            .build();

                    subscriptionStatusAnnounced = true;
                    listenerManager.announce(pnStatus);
                }

                if (result.getMessages().size() != 0) {
                    messageQueue.addAll(result.getMessages());
                }

                timetoken = result.getMetadata().getTimetoken();
                region = result.getMetadata().getRegion();
                startSubscribeLoop();
            }
        });

    }

    private void stopSubscribeLoop() {
        if (subscribeCall != null) {
            subscribeCall.silentCancel();
        }
    }

    private void performHeartbeatLoop() {
        if (heartbeatCall != null) {
            heartbeatCall.silentCancel();
        }

        List presenceChannels = this.subscriptionState.prepareChannelList(false);
        List presenceChannelGroups = this.subscriptionState.prepareChannelGroupList(false);
        Map stateStorage = this.subscriptionState.createStatePayload();

        // do not start the loop if we do not have any presence channels or channel groups enabled.
        if (presenceChannels.isEmpty() && presenceChannelGroups.isEmpty()) {
            return;
        }

        heartbeatCall = new Heartbeat(pubnub, this.retrofitManager.getTransactionInstance())
                .channels(presenceChannels).channelGroups(presenceChannelGroups).state(stateStorage);

        heartbeatCall.async(new PNCallback() {
            @Override
            public void onResponse(Boolean result, PNStatus status) {
                PNHeartbeatNotificationOptions heartbeatVerbosity = pubnub
                        .getConfiguration().getHeartbeatNotificationOptions();

                if (status.isError()) {
                    if (heartbeatVerbosity == PNHeartbeatNotificationOptions.ALL
                            || heartbeatVerbosity == PNHeartbeatNotificationOptions.FAILURES) {
                        listenerManager.announce(status);
                    }

                } else {
                    if (heartbeatVerbosity == PNHeartbeatNotificationOptions.ALL) {
                        listenerManager.announce(status);
                    }
                }
            }
        });

    }

    public final synchronized List getSubscribedChannels() {
        return subscriptionState.prepareChannelList(false);
    }

    public final synchronized List getSubscribedChannelGroups() {
        return subscriptionState.prepareChannelGroupList(false);
    }

    public final synchronized void unsubscribeAll() {
        adaptUnsubscribeBuilder(UnsubscribeOperation.builder()
                .channelGroups(subscriptionState.prepareChannelGroupList(false))
                .channels(subscriptionState.prepareChannelList(false))
                .build());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy