package com.koushikdutta.async.http; import java.util.Arrays; import java.util.HashSet; import org.json.JSONArray; import org.json.JSONObject; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.http.AsyncHttpClient.WebSocketConnectCallback; public class SocketIOClient { public static interface SocketIOConnectCallback { public void onConnectCompleted(Exception ex, SocketIOClient client); } public static interface JSONCallback { public void onJSON(JSONObject json); } public static interface StringCallback { public void onString(String string); } public static interface EventCallback { public void onEvent(String event, JSONArray arguments); } private static void reportError(FutureImpl future, Handler handler, final SocketIOConnectCallback callback, final Exception e) { if (!future.setComplete(e)) return; if (handler != null) { AsyncServer.post(handler, new Runnable() { @Override public void run() { callback.onConnectCompleted(e, null); } }); } else { callback.onConnectCompleted(e, null); } } private void emitRaw(int type, String message) { webSocket.send(String.format("%d:::%s", type, message)); } public void emit(String name, JSONArray args) { final JSONObject event = new JSONObject(); try { event.put("name", name); event.put("args", args); emitRaw(5, event.toString()); } catch (Exception e) { } } public void emit(final String message) { emitRaw(3, message); } public void emit(final JSONObject jsonMessage) { emitRaw(4, jsonMessage.toString()); } private static class FutureImpl extends SimpleFuture<SocketIOClient> { } public static class SocketIORequest extends AsyncHttpPost { String channel; public String getChannel() { return channel; } public SocketIORequest(String uri) { super(Uri.parse(uri).buildUpon().encodedPath("/socket.io/1/").build().toString()); channel = Uri.parse(uri).getPath(); if (TextUtils.isEmpty(channel)) channel = null; } } public static Future<SocketIOClient> connect(final AsyncHttpClient client, String uri, final SocketIOConnectCallback callback) { return connect(client, new SocketIORequest(uri), callback); } public static Future<SocketIOClient> connect(final AsyncHttpClient client, final SocketIORequest request, final SocketIOConnectCallback callback) { final Handler handler = Looper.myLooper() == null ? null : new Handler(); final FutureImpl ret = new FutureImpl(); // dont invoke onto main handler, as it is unnecessary until a session is ready or failed request.setHandler(null); // initiate a session Cancellable cancel = client.execute(request, new AsyncHttpClient.StringCallback() { @Override public void onCompleted(final Exception e, AsyncHttpResponse response, String result) { if (e != null) { reportError(ret, handler, callback, e); return; } try { String[] parts = result.split(":"); String session = parts[0]; final int heartbeat; if (!"".equals(parts[1])) heartbeat = Integer.parseInt(parts[1]) / 2 * 1000; else heartbeat = 0; String transportsLine = parts[3]; String[] transports = transportsLine.split(","); HashSet<String> set = new HashSet<String>(Arrays.asList(transports)); if (!set.contains("websocket")) throw new Exception("websocket not supported"); final String sessionUrl = request.getUri().toString() + "websocket/" + session + "/"; final SocketIOClient socketio = new SocketIOClient(handler, heartbeat, sessionUrl, client); socketio.reconnect(callback, ret); } catch (Exception ex) { reportError(ret, handler, callback, ex); } } }); ret.setParent(cancel); return ret; } CompletedCallback closedCallback; public CompletedCallback getClosedCallback() { return closedCallback; } public void setClosedCallback(CompletedCallback callback) { closedCallback = callback; } JSONCallback jsonCallback; public JSONCallback getJSONCallback() { return jsonCallback; } public void setJSONCallback(JSONCallback callback) { jsonCallback = callback; } StringCallback stringCallback; public StringCallback getStringCallback() { return stringCallback; } public void setStringCallback(StringCallback callback) { stringCallback = callback; } EventCallback eventCallback; public EventCallback getEventCallback() { return eventCallback; } public void setEventCallback(EventCallback callback) { eventCallback = callback; } String sessionUrl; WebSocket webSocket; AsyncHttpClient httpClient; private SocketIOClient(Handler handler, int heartbeat, String sessionUrl, AsyncHttpClient httpCliet) { this.handler = handler; this.heartbeat = heartbeat; this.sessionUrl = sessionUrl; this.httpClient = httpCliet; } public boolean isConnected() { return connected && !disconnected && webSocket != null && webSocket.isOpen(); } public void disconnect() { webSocket.setStringCallback(null); webSocket.setDataCallback(null); webSocket.setClosedCallback(null); webSocket.close(); webSocket = null; } private void reconnect(final SocketIOConnectCallback callback, final FutureImpl ret) { if (isConnected()) { httpClient.getServer().post(new Runnable() { @Override public void run() { ret.setComplete(new Exception("already connected")); } }); return; } connected = false; disconnected = false; Cancellable cancel = httpClient.websocket(sessionUrl, null, new WebSocketConnectCallback() { @Override public void onCompleted(Exception ex, WebSocket webSocket) { if (ex != null) { reportError(ret, handler, callback, ex); return; } SocketIOClient.this.webSocket = webSocket; attach(callback, ret); } }); ret.setParent(cancel); } private Future<SocketIOClient> reconnect(final SocketIOConnectCallback callback) { FutureImpl ret = new FutureImpl(); reconnect(callback, ret); return ret; } boolean connected; boolean disconnected; int heartbeat; void setupHeartbeat() { final WebSocket ws = webSocket; Runnable heartbeatRunner = new Runnable() { @Override public void run() { if (heartbeat <= 0 || disconnected || !connected || ws != webSocket || ws == null || !ws.isOpen()) return; webSocket.send("2:::"); webSocket.getServer().postDelayed(this, heartbeat); } }; heartbeatRunner.run(); } Handler handler; private void attach(final SocketIOConnectCallback callback, final FutureImpl future) { webSocket.setDataCallback(new NullDataCallback()); webSocket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(final Exception ex) { disconnected = true; webSocket = null; Runnable runner = new Runnable() { @Override public void run() { if (!connected) { // closed connection before open... callback.onConnectCompleted(ex == null ? new Exception("connection failed") : ex, null); } else if (!disconnected) { if (closedCallback != null) closedCallback.onCompleted(ex == null ? new Exception("connection failed") : ex); } } }; if (handler != null) { AsyncServer.post(handler, runner); } else { runner.run(); } } }); webSocket.setStringCallback(new WebSocket.StringCallback() { @Override public void onStringAvailable(String message) { try { // Log.d(TAG, "Message: " + message); String[] parts = message.split(":", 4); int code = Integer.parseInt(parts[0]); switch (code) { case 0: if (!connected) throw new Exception("received disconnect before client connect"); disconnected = true; // disconnect webSocket.close(); if (closedCallback != null) { if (handler != null) { AsyncServer.post(handler, new Runnable() { @Override public void run() { closedCallback.onCompleted(null); } }); } else { closedCallback.onCompleted(null); } } break; case 1: // connect if (connected) throw new Exception("received duplicate connect event"); if (!future.setComplete(SocketIOClient.this)) throw new Exception("request canceled"); connected = true; setupHeartbeat(); callback.onConnectCompleted(null, SocketIOClient.this); break; case 2: // heartbeat webSocket.send("2::"); break; case 3: { if (!connected) throw new Exception("received message before client connect"); // message final String messageId = parts[1]; final String dataString = parts[3]; // ack if(!"".equals(messageId)) { webSocket.send(String.format("6:::%s", messageId)); } if (stringCallback != null) { if (handler != null) { AsyncServer.post(handler, new Runnable() { @Override public void run() { stringCallback.onString(dataString); } }); } else { stringCallback.onString(dataString); } } break; } case 4: { if (!connected) throw new Exception("received message before client connect"); //json message final String messageId = parts[1]; final String dataString = parts[3]; final JSONObject jsonMessage = new JSONObject(dataString); // ack if(!"".equals(messageId)) { webSocket.send(String.format("6:::%s", messageId)); } if (jsonCallback != null) { if (handler != null) { AsyncServer.post(handler, new Runnable() { @Override public void run() { jsonCallback.onJSON(jsonMessage); } }); } else { jsonCallback.onJSON(jsonMessage); } } break; } case 5: { if (!connected) throw new Exception("received message before client connect"); final String messageId = parts[1]; final String dataString = parts[3]; JSONObject data = new JSONObject(dataString); final String event = data.getString("name"); final JSONArray args = data.optJSONArray("args"); // ack if(!"".equals(messageId)) { webSocket.send(String.format("6:::%s", messageId)); } if (eventCallback != null) { if (handler != null) { AsyncServer.post(handler, new Runnable() { @Override public void run() { eventCallback.onEvent(event, args); } }); } else { eventCallback.onEvent(event, args); } } break; } case 6: // ACK break; case 7: // error throw new Exception(message); case 8: // noop break; default: throw new Exception("unknown code"); } } catch (Exception ex) { webSocket.close(); if (!connected) { reportError(future, handler, callback, ex); } else { disconnected = true; if (closedCallback != null) { closedCallback.onCompleted(ex); } } } } }); } }