package com.joyplus.faye; /* The MIT License * * Copyright (c) 2011 Paul Crawford * Copyright (c) 2013 Saul Howard * * Ported from Objective-C to Java by Saul Howard <saulpower1@gmail.com> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import android.os.Handler; import com.joyplus.widget.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.joyplus.faye.WebSocketClient.Listener; import java.net.URI; import java.util.Date; public class FayeClient implements Listener { private final String TAG = this.getClass().getSimpleName(); private static final String HANDSHAKE_CHANNEL = "/meta/handshake"; private static final String CONNECT_CHANNEL = "/meta/connect"; private static final String DISCONNECT_CHANNEL = "/meta/disconnect"; private static final String SUBSCRIBE_CHANNEL = "/meta/subscribe"; private static final String UNSUBSCRIBE_CHANNEL = "/meta/unsubscribe"; private static final String KEY_CHANNEL = "channel"; private static final String KEY_SUCCESS = "successful"; private static final String KEY_CLIENT_ID = "clientId"; private static final String KEY_VERSION = "version"; private static final String KEY_MIN_VERSION = "minimumVersion"; private static final String KEY_SUBSCRIPTION = "subscription"; private static final String KEY_SUP_CONN_TYPES = "supportedConnectionTypes"; private static final String KEY_CONN_TYPE = "connectionType"; private static final String KEY_DATA = "data"; private static final String KEY_ID = "id"; private static final String KEY_EXT = "ext"; private static final String KEY_ERROR = "error"; private static final String VALUE_VERSION = "1.0"; private static final String VALUE_MIN_VERSION = "1.0beta"; private static final String VALUE_CONN_TYPE = "websocket"; private static final long RECONNECT_WAIT = 10000; private static final int MAX_CONNECTION_ATTEMPTS = 0; private WebSocketClient mClient; private boolean mConnected = false; private int mConnectionAttempts = 0; private URI mFayeUrl; private static String mFayeClientId; private String mActiveSubChannel; private JSONObject mConnectionExtension; private boolean mRunning = false; private boolean mReconnecting = false; private Handler mHandler; private Runnable mConnectionMonitor = new Runnable() { @Override public void run() { if (!mConnected) { if (mConnectionAttempts < MAX_CONNECTION_ATTEMPTS) { openWebSocketConnection(); mConnectionAttempts++; getHandler().postDelayed(this, RECONNECT_WAIT); } } else { getHandler().removeCallbacks(this); mRunning = false; mConnectionAttempts = 0; mReconnecting = false; } } }; private FayeListener mFayeListener; /** * Register a callback to be invoked for specific Faye client events * * @param mFayeListener * The callback that will run */ public void setFayeListener(FayeListener mFayeListener) { this.mFayeListener = mFayeListener; } private Handler getHandler() { return mHandler; } /** * Creates a new Faye Client for communicating with a Faye server at the * provided URL and the specified channel. * * @param fayeUrl * The URL of the FayeServer * @param channel * The channel to subscribe to */ public FayeClient(Handler handler, URI fayeUrl, String channel) { mHandler = handler; mFayeUrl = fayeUrl; mActiveSubChannel = channel; } /** * Connect to a server using the extension authentication object * * @param extension * Bayeux extension authentication that exchanges authentication * credentials and tokens within Bayeux messages ext fields */ public void connectToServer(JSONObject extension) { mConnectionExtension = extension; openWebSocketConnection(); } public void disconnectFromServer() { disconnect(); } /** * Sends events on a channel by sending an event message * * @param json * JSON object containing message to be sent to server */ public void sendMessage(JSONObject json) { publish(json, mConnectionExtension); } private void openWebSocketConnection() { if (mClient != null) { mClient.disconnect(); mClient = null; } mClient = new WebSocketClient(getHandler(), mFayeUrl, this, null); mClient.connect(); } public void closeWebSocketConnection() { Log.i(TAG, "socket disconnected"); mClient.disconnect(); } private void resetWebSocketConnection() { if (!mConnected) { if (!mRunning) { getHandler().post(mConnectionMonitor); } } } /** * Initiates a connection negotiation by sending a message to the * "/meta/handshake" channel. * * Example JSON { KEY_CHANNEL: "/meta/handshake", KEY_VERSION: "1.0", * KEY_MIN_VERSION: "1.0beta", KEY_SUP_CONN_TYPES: ["long-polling", * "callback-polling", "iframe", "websocket] } */ private void handshake() { try { JSONArray connTypes = new JSONArray(); connTypes.put("long-polling"); connTypes.put("callback-polling"); connTypes.put("iframe"); connTypes.put("websocket"); JSONObject json = new JSONObject(); json.put(KEY_CHANNEL, HANDSHAKE_CHANNEL); json.put(KEY_VERSION, VALUE_VERSION); json.put(KEY_MIN_VERSION, VALUE_MIN_VERSION); json.put(KEY_SUP_CONN_TYPES, connTypes); mClient.send(json.toString()); } catch (JSONException ex) { Log.e(TAG, "Handshake Failed", ex); } } /** * After a Bayeux client has discovered the server's capabilities with a * handshake exchange, a connection is established by sending a message to * the "/meta/connect" channel. * * Example JSON { KEY_CHANNEL: "/meta/connect", KEY_CLIENT_ID: * "Un1q31d3nt1f13r", KEY_CONN_TYPES: "long-polling" } */ public void connect() { try { JSONObject json = new JSONObject(); json.put(KEY_CHANNEL, CONNECT_CHANNEL); json.put(KEY_CLIENT_ID, mFayeClientId); json.put(KEY_CONN_TYPE, VALUE_CONN_TYPE); mClient.send(json.toString()); } catch (JSONException ex) { Log.e(TAG, "Handshake Failed", ex); } } /** * Cease operation by sending a request to the "/meta/disconnect" channel * for the server to remove any client-related state. * * Example JSON { KEY_CHANNEL: "/meta/disconnect", KEY_CLIENT_ID: * "Un1q31d3nt1f13r" } */ public void disconnect() { Log.i(TAG, "socket disconnected"); try { JSONObject json = new JSONObject(); json.put(KEY_CHANNEL, DISCONNECT_CHANNEL); json.put(KEY_CLIENT_ID, mFayeClientId); mClient.send(json.toString()); } catch (JSONException ex) { Log.e(TAG, "Handshake Failed", ex); } } /** * Register interest in a channel and request that messages published to * that channel are delivered. * * Example JSON { KEY_CHANNEL: "/meta/subscribe", KEY_CLIENT_ID: * "Un1q31d3nt1f13r", KEY_SUBSCRIPTION: "/foo/ **" } */ public void subscribe() { try { JSONObject json = new JSONObject(); json.put(KEY_CHANNEL, SUBSCRIBE_CHANNEL); json.put(KEY_CLIENT_ID, mFayeClientId); json.put(KEY_SUBSCRIPTION, mActiveSubChannel); if (null != mConnectionExtension) { json.put(KEY_EXT, mConnectionExtension); } mClient.send(json.toString()); } catch (JSONException ex) { Log.e(TAG, "Handshake Failed", ex); } } /** * Send unsubscribe messages to cancel interest in channel and to request * that messages published to that channel are not delivered. * * Example JSON { KEY_CHANNEL: "/meta/unsubscribe", KEY_CLIENT_ID: * "Un1q31d3nt1f13r", KEY_SUBSCRIPTION: "/foo/**" } */ public void unsubscribe() { try { JSONObject json = new JSONObject(); json.put(KEY_CHANNEL, UNSUBSCRIBE_CHANNEL); json.put(KEY_CLIENT_ID, mFayeClientId); json.put(KEY_SUBSCRIPTION, mActiveSubChannel); mClient.send(json.toString()); } catch (JSONException ex) { Log.e(TAG, "Handshake Failed", ex); } } /** * Publish events on a channel by sending an event message * * Example JSON { KEY_CHANNEL: "/some/channel", KEY_CLIENT_ID: * "Un1q31d3nt1f13r", KEY_DATA: * "some application string or JSON encoded object", KEY_ID: * "some unique message id" } * * @param message * JSON object containing message to be sent to server * * @param extension * Bayeux extension authentication that exchanges authentication * credentials and tokens within Bayeux messages ext fields */ public void publish(JSONObject message, JSONObject extension) { String channel = mActiveSubChannel; long number = (new Date()).getTime(); String messageId = String.format("msg_%d_%d", number, 1); try { JSONObject json = new JSONObject(); json.put(KEY_CHANNEL, channel); json.put(KEY_CLIENT_ID, mFayeClientId); json.put(KEY_DATA, message); json.put(KEY_ID, messageId); if (null != extension) { json.put(KEY_EXT, extension); } mClient.send(json.toString()); } catch (JSONException ex) { Log.e(TAG, "Handshake Failed", ex); } } /* * (non-Javadoc) * * @see com.saulpower.fayeclient.WebSocketClient.Listener#onConnect() */ @Override public void onConnect() { mConnected = true; mReconnecting = false; handshake(); } /* * (non-Javadoc) * * @see * com.saulpower.fayeclient.WebSocketClient.Listener#onMessage(java.lang * .String) */ @Override public void onMessage(String message) { parseFayeMessage(message); } /* * (non-Javadoc) * * @see com.saulpower.fayeclient.WebSocketClient.Listener#onMessage(byte[]) */ @Override public void onMessage(byte[] data) { Log.i(TAG, "Data message"); } /* * (non-Javadoc) * * @see com.saulpower.fayeclient.WebSocketClient.Listener#onDisconnect(int, * java.lang.String) */ @Override public void onDisconnect(int code, String reason) { mConnected = false; if (mFayeListener != null) { mFayeListener.disconnectedFromServer(); } } /* * (non-Javadoc) * * @see * com.saulpower.fayeclient.WebSocketClient.Listener#onError(java.lang.Exception * ) */ @Override public void onError(Exception error) { Log.w(TAG, "resetWebSocketConnection " + error.getMessage(), error); if (!mReconnecting) { mReconnecting = true; mConnected = false; resetWebSocketConnection(); } } /** * Parse the Faye message and call the appropriate listener method. * * @param message * A json string from the Faye server */ private void parseFayeMessage(String message) { try { JSONArray messageArray = new JSONArray(message); for (int i = 0; i < messageArray.length(); i++) { JSONObject fayeMessage = messageArray.optJSONObject(i); if (fayeMessage == null) continue; String channel = fayeMessage.optString(KEY_CHANNEL); boolean success = fayeMessage.optBoolean(KEY_SUCCESS); if (channel.equals(HANDSHAKE_CHANNEL)) { if (success) { mFayeClientId = fayeMessage.optString(KEY_CLIENT_ID); if (mFayeListener != null) { mFayeListener.connectedToServer(); } connect(); subscribe(); } // else if (BuildConfig.DEBUG) Log.d(TAG, // "Error with Handshake"); return; } if (channel.equals(CONNECT_CHANNEL)) { if (success) { mConnected = true; connect(); } // else if (BuildConfig.DEBUG) Log.d(TAG, // "Error Connecting to Faye"); return; } if (channel.equals(DISCONNECT_CHANNEL)) { if (success) { mConnected = false; closeWebSocketConnection(); if (mFayeListener != null) { mFayeListener.disconnectedFromServer(); } } // else if (BuildConfig.DEBUG) Log.d(TAG, // "Error Disconnecting to Faye"); return; } if (channel.equals(SUBSCRIBE_CHANNEL)) { if (success) { if (mFayeListener != null) { mFayeListener.subscribedToChannel(fayeMessage .optString(KEY_SUBSCRIPTION)); } } return; } if (channel.equals(UNSUBSCRIBE_CHANNEL)) { if (success) { // if (BuildConfig.DEBUG) Log.d(TAG, // String.format("Unsubscribed from channel %s on Faye", // fayeMessage.optString(KEY_SUBSCRIPTION))); } // else if (BuildConfig.DEBUG) Log.d(TAG, // "Error Connecting to Faye"); return; } if (isSubscribedToChannel(channel)) { JSONObject data = null; if ((data = fayeMessage.optJSONObject(KEY_DATA)) != null && mFayeListener != null) { mFayeListener.messageReceived(data); } return; } // if (BuildConfig.DEBUG) Log.d(TAG, // String.format("No match for channel %s", channel)); } } catch (JSONException ex) { Log.e(TAG, "Could not parse faye message", ex); } } /** * Checks to see if we are subscribed to the passed in channel * * @param channel * Name of channel to check * @return True if we are connected to the passed in channel, false * otherwise */ private boolean isSubscribedToChannel(String channel) { boolean isSubscribed = false; if (mActiveSubChannel != null && mActiveSubChannel.length() > 0 && channel != null && channel.length() > 0) { String[] subscribedChannelSegments = mActiveSubChannel.split("/"); String[] channelSegments = channel.split("/"); int i = 0; isSubscribed = true; do { String s1 = subscribedChannelSegments[i]; String s2 = (i < channelSegments.length ? channelSegments[i] : null); if (s2 == null) break; if (!s2.equals(s1)) { if (s1.equals("**")) { break; } else { isSubscribed = false; } } i++; } while (isSubscribed && i < subscribedChannelSegments.length); } return isSubscribed; } public interface FayeListener { void connectedToServer(); void disconnectedFromServer(); void subscribedToChannel(String subscription); void subscriptionFailedWithError(String error); void messageReceived(JSONObject json); } }