package ua.naiksoftware.stomp; import android.support.annotation.Nullable; import android.util.Log; import java.util.concurrent.TimeUnit; import io.reactivex.Scheduler; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import ua.naiksoftware.stomp.dto.StompCommand; import ua.naiksoftware.stomp.dto.StompHeader; import ua.naiksoftware.stomp.dto.StompMessage; public class HeartBeatTask { private static final String TAG = HeartBeatTask.class.getSimpleName(); private Scheduler scheduler; private int serverHeartbeat = 0; private int clientHeartbeat = 0; private int serverHeartbeatNew = 0; private int clientHeartbeatNew = 0; private transient long lastServerHeartBeat = 0; private transient Disposable clientSendHeartBeatTask; private transient Disposable serverCheckHeartBeatTask; private FailedListener failedListener; private SendCallback sendCallback; public HeartBeatTask(SendCallback sendCallback, @Nullable FailedListener failedListener) { this.failedListener = failedListener; this.sendCallback = sendCallback; } public void setServerHeartbeat(int serverHeartbeat) { this.serverHeartbeatNew = serverHeartbeat; } public void setClientHeartbeat(int clientHeartbeat) { this.clientHeartbeatNew = clientHeartbeat; } public int getServerHeartbeat() { return serverHeartbeatNew; } public int getClientHeartbeat() { return clientHeartbeatNew; } public boolean consumeHeartBeat(StompMessage message) { switch (message.getStompCommand()) { case StompCommand.CONNECTED: heartBeatHandshake(message.findHeader(StompHeader.HEART_BEAT)); break; case StompCommand.SEND: abortClientHeartBeatSend(); break; case StompCommand.MESSAGE: //a MESSAGE works as an hear-beat too. abortServerHeartBeatCheck(); break; case StompCommand.UNKNOWN: if ("\n".equals(message.getPayload())) { Log.d(TAG, "<<< PONG"); abortServerHeartBeatCheck(); return false; } break; } return true; } public void shutdown() { if (clientSendHeartBeatTask != null) { clientSendHeartBeatTask.dispose(); } if (serverCheckHeartBeatTask != null) { serverCheckHeartBeatTask.dispose(); } lastServerHeartBeat = 0; } /** * Analise heart-beat sent from server (if any), to adjust the frequency. * Startup the heart-beat logic. */ private void heartBeatHandshake(final String heartBeatHeader) { if (heartBeatHeader != null) { // The heart-beat header is OPTIONAL final String[] heartbeats = heartBeatHeader.split(","); if (clientHeartbeatNew > 0) { //there will be heart-beats every MAX(<cx>,<sy>) milliseconds clientHeartbeat = Math.max(clientHeartbeatNew, Integer.parseInt(heartbeats[1])); } if (serverHeartbeatNew > 0) { //there will be heart-beats every MAX(<cx>,<sy>) milliseconds serverHeartbeat = Math.max(serverHeartbeatNew, Integer.parseInt(heartbeats[0])); } } if (clientHeartbeat > 0 || serverHeartbeat > 0) { scheduler = Schedulers.io(); if (clientHeartbeat > 0) { //client MUST/WANT send heart-beat Log.d(TAG, "Client will send heart-beat every " + clientHeartbeat + " ms"); scheduleClientHeartBeat(); } if (serverHeartbeat > 0) { Log.d(TAG, "Client will listen to server heart-beat every " + serverHeartbeat + " ms"); //client WANT to listen to server heart-beat scheduleServerHeartBeatCheck(); // initialize the server heartbeat lastServerHeartBeat = System.currentTimeMillis(); } } } private void scheduleServerHeartBeatCheck() { if (serverHeartbeat > 0 && scheduler != null) { final long now = System.currentTimeMillis(); Log.d(TAG, "Scheduling server heart-beat to be checked in " + serverHeartbeat + " ms and now is '" + now + "'"); //add some slack on the check serverCheckHeartBeatTask = scheduler.scheduleDirect(() -> checkServerHeartBeat(), serverHeartbeat, TimeUnit.MILLISECONDS); } } private void checkServerHeartBeat() { if (serverHeartbeat > 0) { final long now = System.currentTimeMillis(); //use a forgiving boundary as some heart beats can be delayed or lost. final long boundary = now - (3 * serverHeartbeat); //we need to check because the task could failed to abort if (lastServerHeartBeat < boundary) { Log.d(TAG, "It's a sad day ;( Server didn't send heart-beat on time. Last received at '" + lastServerHeartBeat + "' and now is '" + now + "'"); if (failedListener != null) { failedListener.onServerHeartBeatFailed(); } } else { Log.d(TAG, "We were checking and server sent heart-beat on time. So well-behaved :)"); lastServerHeartBeat = System.currentTimeMillis(); } } } /** * Used to abort the server heart-beat check. */ private void abortServerHeartBeatCheck() { lastServerHeartBeat = System.currentTimeMillis(); Log.d(TAG, "Aborted last check because server sent heart-beat on time ('" + lastServerHeartBeat + "'). So well-behaved :)"); if (serverCheckHeartBeatTask != null) { serverCheckHeartBeatTask.dispose(); } scheduleServerHeartBeatCheck(); } /** * Schedule a client heart-beat if clientHeartbeat > 0. */ private void scheduleClientHeartBeat() { if (clientHeartbeat > 0 && scheduler != null) { Log.d(TAG, "Scheduling client heart-beat to be sent in " + clientHeartbeat + " ms"); clientSendHeartBeatTask = scheduler.scheduleDirect(() -> sendClientHeartBeat(), clientHeartbeat, TimeUnit.MILLISECONDS); } } /** * Send the raw heart-beat to the server. */ private void sendClientHeartBeat() { sendCallback.sendClientHeartBeat("\r\n"); Log.d(TAG, "PING >>>"); //schedule next client heart beat this.scheduleClientHeartBeat(); } /** * Used when we have a scheduled heart-beat and we send a new message to the server. * The new message will work as an heart-beat so we can abort current one and schedule another */ private void abortClientHeartBeatSend() { if (clientSendHeartBeatTask != null) { clientSendHeartBeatTask.dispose(); } scheduleClientHeartBeat(); } public interface FailedListener { void onServerHeartBeatFailed(); } public interface SendCallback { void sendClientHeartBeat(String pingMessage); } }