44import android .support .annotation .Nullable ;
55import android .util .Log ;
66
7+ import java .util .concurrent .TimeUnit ;
8+
79import io .reactivex .Completable ;
810import io .reactivex .Observable ;
11+ import io .reactivex .Scheduler ;
12+ import io .reactivex .disposables .Disposable ;
13+ import io .reactivex .schedulers .Schedulers ;
914import io .reactivex .subjects .PublishSubject ;
15+ import ua .naiksoftware .stomp .client .StompCommand ;
16+ import ua .naiksoftware .stomp .client .StompMessage ;
1017
1118/**
1219 * Created by forresthopkinsa on 8/8/2017.
@@ -18,6 +25,16 @@ abstract class AbstractConnectionProvider implements ConnectionProvider {
1825
1926 private static final String TAG = AbstractConnectionProvider .class .getSimpleName ();
2027
28+ private transient Disposable clientSendHeartBeatTask ;
29+ private transient Disposable serverCheckHeartBeatTask ;
30+ private Scheduler scheduler ;
31+
32+ private int serverHeartbeat = 0 ;
33+ private int clientHeartbeat = 0 ;
34+
35+ private transient long lastServerHeartBeat = 0 ;
36+
37+
2138 @ NonNull
2239 private final PublishSubject <LifecycleEvent > mLifecycleStream ;
2340 @ NonNull
@@ -46,6 +63,11 @@ public Observable<String> messages() {
4663
4764 @ Override
4865 public Completable disconnect () {
66+ if (clientSendHeartBeatTask != null ) {
67+ clientSendHeartBeatTask .dispose ();
68+ }
69+ scheduler .shutdown ();
70+ Log .d (TAG , "Shutting down heart-beat scheduler..." );
4971 return Completable
5072 .fromAction (this ::rawDisconnect );
5173 }
@@ -55,11 +77,6 @@ private Completable initSocket() {
5577 .fromAction (this ::createWebSocketConnection );
5678 }
5779
58- // Doesn't do anything at all, only here as a stub
59- public Completable setHeartbeat (int ms ) {
60- return Completable .complete ();
61- }
62-
6380 /**
6481 * Most important method: connects to websocket and notifies program of messages.
6582 * <p>
@@ -111,13 +128,152 @@ void emitLifecycleEvent(@NonNull LifecycleEvent lifecycleEvent) {
111128 }
112129
113130 void emitMessage (String stompMessage ) {
114- Log .d (TAG , "Emit STOMP message: " + stompMessage );
115- mMessagesStream .onNext (stompMessage );
131+ //TODO: Why we don't publish a StompMessage, instead of String? will this connection provider work with other protocol?
132+ final StompMessage sm = StompMessage .from (stompMessage );
133+ if (StompCommand .CONNECTED .equals (sm .getStompCommand ())) {
134+ heartBeatHandshake (sm .findHeader (StompHeader .HEART_BEAT ));
135+ } else if (StompCommand .SEND .equals (sm .getStompCommand ())) {
136+ abortClientHeartBeatSend ();
137+ } else if (StompCommand .MESSAGE .equals (sm .getStompCommand ())) {
138+ //a MESSAGE works as an hear-beat too.
139+ abortServerHeartBeatCheck ();
140+ }
141+
142+ if (stompMessage .equals ("\n " )) {
143+ Log .d (TAG , "<<< PONG" );
144+ abortServerHeartBeatCheck ();
145+ } else {
146+ Log .d (TAG , "Receive STOMP message: " + stompMessage );
147+ mMessagesStream .onNext (stompMessage );
148+ }
116149 }
117150
118151 @ NonNull
119152 @ Override
120153 public Observable <LifecycleEvent > lifecycle () {
121154 return mLifecycleStream ;
122155 }
156+
157+ /**
158+ * Analise heart-beat sent from server (if any), to adjust the frequency.
159+ * Startup the heart-beat logic.
160+ *
161+ * @param heartBeatHeader
162+ */
163+ private void heartBeatHandshake (final String heartBeatHeader ) {
164+ if (heartBeatHeader != null ) {
165+ // The heart-beat header is OPTIONAL
166+ final String [] heartbeats = heartBeatHeader .split ("," );
167+ if (clientHeartbeat > 0 ) {
168+ //there will be heart-beats every MAX(<cx>,<sy>) milliseconds
169+ clientHeartbeat = Math .max (clientHeartbeat , Integer .parseInt (heartbeats [1 ]));
170+ }
171+ if (serverHeartbeat > 0 ) {
172+ //there will be heart-beats every MAX(<cx>,<sy>) milliseconds
173+ serverHeartbeat = Math .max (serverHeartbeat , Integer .parseInt (heartbeats [0 ]));
174+ }
175+ }
176+ if (clientHeartbeat > 0 || serverHeartbeat > 0 ) {
177+ scheduler = Schedulers .io ();
178+
179+ if (clientHeartbeat > 0 ) {
180+ //client MUST/WANT send heart-beat
181+ Log .d (TAG , "Client will send heart-beat every " + clientHeartbeat + " ms" );
182+ scheduleClientHeartBeat ();
183+ }
184+ if (serverHeartbeat > 0 ) {
185+ Log .d (TAG , "Client will listen to server heart-beat every " + serverHeartbeat + " ms" );
186+ //client WANT to listen to server heart-beat
187+ scheduleServerHeartBeatCheck ();
188+ }
189+ }
190+ }
191+
192+ protected void scheduleServerHeartBeatCheck () {
193+ if (serverHeartbeat > 0 && scheduler != null ) {
194+ Log .d (TAG , "Scheduling server heart-beat to be checked in " + serverHeartbeat + " ms" );
195+ //add some slack on the check
196+ serverCheckHeartBeatTask = scheduler .scheduleDirect (() ->
197+ checkServerHeartBeat (), serverHeartbeat , TimeUnit .MILLISECONDS );
198+ }
199+ }
200+
201+ private void checkServerHeartBeat () {
202+ if (serverHeartbeat > 0 ) {
203+ final long now = System .currentTimeMillis ();
204+ //use a forgiving boundary as some heart beats can be delayed or lost.
205+ final long boundary = now - (3 * serverHeartbeat );
206+ //we need to check because the task could failed to abort
207+ if (lastServerHeartBeat < boundary ) {
208+ Log .d (TAG , "It's a sad day ;( Server didn't send heart-beat on time. Last received at '" + lastServerHeartBeat + "' and now is '" + now + "'" );
209+ final LifecycleEvent failedServerHeartBeat = new LifecycleEvent (LifecycleEvent .Type .FAILED_SERVER_HEARTBEAT );
210+ emitLifecycleEvent (failedServerHeartBeat );
211+ } else {
212+ Log .d (TAG , "We were checking and server sent heart-beat on time. So well-behaved :)" );
213+ lastServerHeartBeat = System .currentTimeMillis ();
214+ }
215+ }
216+ }
217+
218+ /**
219+ * Used to abort the server heart-beat check.
220+ */
221+ private void abortServerHeartBeatCheck () {
222+ lastServerHeartBeat = System .currentTimeMillis ();
223+ Log .d (TAG , "Aborted last check because server sent heart-beat on time ('" + lastServerHeartBeat + "'). So well-behaved :)" );
224+ if (serverCheckHeartBeatTask != null ) {
225+ serverCheckHeartBeatTask .dispose ();
226+ }
227+ scheduleServerHeartBeatCheck ();
228+ }
229+
230+ /**
231+ * Schedule a client heart-beat if clientHeartbeat > 0.
232+ */
233+ public void scheduleClientHeartBeat () {
234+ if (clientHeartbeat > 0 && scheduler != null ) {
235+ Log .d (TAG , "Scheduling client heart-beat to be sent in " + clientHeartbeat + " ms" );
236+ clientSendHeartBeatTask = scheduler .scheduleDirect (() ->
237+ sendClientHeartBeat (), clientHeartbeat , TimeUnit .MILLISECONDS );
238+ }
239+ }
240+
241+ /**
242+ * Send the raw heart-beat to the server.
243+ */
244+ private void sendClientHeartBeat () {
245+ this .rawSend ("\r \n " );
246+ Log .d (TAG , "PING >>>" );
247+ //schedule next client heart beat
248+ this .scheduleClientHeartBeat ();
249+ }
250+
251+ /**
252+ * Used when we have a scheduled heart-beat and we send a new message to the server.
253+ * The new message will work as an heart-beat so we can abort current one and schedule another
254+ */
255+ private void abortClientHeartBeatSend () {
256+ if (clientSendHeartBeatTask != null ) {
257+ clientSendHeartBeatTask .dispose ();
258+ }
259+ scheduleClientHeartBeat ();
260+ }
261+
262+ /**
263+ * Set the server heart-beat
264+ *
265+ * @param ms milliseconds
266+ */
267+ public void setServerHeartbeat (int ms ) {
268+ this .serverHeartbeat = ms ;
269+ }
270+
271+ /**
272+ * Set the client heart-beat
273+ *
274+ * @param ms milliseconds
275+ */
276+ public void setClientHeartbeat (int ms ) {
277+ this .clientHeartbeat = ms ;
278+ }
123279}
0 commit comments