diff --git a/.gitignore b/.gitignore
index 81d81b4..a361a6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
-gen/
+bin
+gen
+.classpath
+.project
local.properties
+/.settings
+project.properties
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index eb6f10c..80e8295 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4,4 +4,8 @@
package="com.codebutler.android_websockets"
android:versionCode="1"
android:versionName="0.01">
+
+
diff --git a/README.md b/README.md
index 17a934d..26aa1f7 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,36 @@
-# WebSocket client for Android
+# THIS LIBRARY IS DEPRECATED IN FAVOR OF:
-A very simple bare-minimum WebSocket client for Android.
+[AndroidAsync](https://github.com/koush/AndroidAsync)
+
+
+
+
+
+
+
+
+# WebSocket and Socket.IO client for Android
## Credits
The hybi parser is based on code from the [faye project](https://github.com/faye/faye-websocket-node). Faye is Copyright (c) 2009-2012 James Coglan. Many thanks for the great open-source library!
-Ported from JavaScript to Java by [Eric Butler](https://twitter.com/codebutler) .
+The hybi parser was ported from JavaScript to Java by [Eric Butler](https://twitter.com/codebutler) .
-## Usage
+The WebSocket client was written by [Eric Butler](https://twitter.com/codebutler) .
-Here's the entire API:
+The Socket.IO client was written by [Koushik Dutta](https://twitter.com/koush).
+
+The Socket.IO client component was ported from Koushik Dutta's AndroidAsync(https://github.com/koush/AndroidAsync) by [Vinay S Shenoy](https://twitter.com/vinaysshenoy)
+
+## WebSocket Usage
```java
List extraHeaders = Arrays.asList(
- new BasicNameValuePair("Cookie", "session=abcd");
+ new BasicNameValuePair("Cookie", "session=abcd")
);
-WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Handler() {
+WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Listener() {
@Override
public void onConnect() {
Log.d(TAG, "Connected!");
@@ -30,7 +43,7 @@ WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), n
@Override
public void onMessage(byte[] data) {
- Log.d(TAG, String.format("Got binary message! %s", toHexString(data));
+ Log.d(TAG, String.format("Got binary message! %s", toHexString(data)));
}
@Override
@@ -42,6 +55,7 @@ WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), n
public void onError(Exception error) {
Log.e(TAG, "Error!", error);
}
+
}, extraHeaders);
client.connect();
@@ -52,6 +66,94 @@ client.send(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF });
client.disconnect();
```
+## Socket.IO Usage
+
+```java
+SocketIOClient.connect("http://localhost:80", new ConnectCallback() {
+
+ @Override
+ public void onConnectCompleted(Exception ex, SocketIOClient client) {
+
+ if (ex != null) {
+ return;
+ }
+
+ //Save the returned SocketIOClient instance into a variable so you can disconnect it later
+ client.setDisconnectCallback(MainActivity.this);
+ client.setErrorCallback(MainActivity.this);
+ client.setJSONCallback(MainActivity.this);
+ client.setStringCallback(MainActivity.this);
+
+ //You need to explicitly specify which events you are interested in receiving
+ client.addListener("news", MainActivity.this);
+
+ client.of("/chat", new ConnectCallback() {
+
+ @Override
+ public void onConnectCompleted(Exception ex, SocketIOClient client) {
+
+ if (ex != null) {
+ ex.printStackTrace();
+ return;
+ }
+
+ //This client instance will be using the same websocket as the original client,
+ //but will point to the indicated endpoint
+ client.setDisconnectCallback(MainActivity.this);
+ client.setErrorCallback(MainActivity.this);
+ client.setJSONCallback(MainActivity.this);
+ client.setStringCallback(MainActivity.this);
+ client.addListener("a message", MainActivity.this);
+
+ }
+ });
+
+ }
+}, new Handler());
+
+
+@Override
+public void onEvent(String event, JSONArray argument, Acknowledge acknowledge) {
+ try {
+ Log.d("MainActivity", "Event:" + event + "Arguments:"
+ + argument.toString(2));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+}
+
+@Override
+public void onString(String string, Acknowledge acknowledge) {
+ Log.d("MainActivity", string);
+
+}
+
+@Override
+public void onJSON(JSONObject json, Acknowledge acknowledge) {
+ try {
+ Log.d("MainActivity", "json:" + json.toString(2));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+}
+
+@Override
+public void onError(String error) {
+ Log.d("MainActivity", error);
+
+}
+
+@Override
+public void onDisconnect(Exception e) {
+ Log.d(mComponentTag, "Disconnected:" + e.getMessage());
+
+}
+
+```
+
+
## TODO
@@ -64,6 +166,7 @@ client.disconnect();
Copyright (c) 2009-2012 James Coglan
Copyright (c) 2012 Eric Butler
+ Copyright (c) 2012 Koushik Dutta
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
diff --git a/build.xml b/build.xml
index 7db9217..876bc4b 100644
--- a/build.xml
+++ b/build.xml
@@ -1,5 +1,5 @@
-
+
+
+
+
+
+
+
diff --git a/proguard-project.txt b/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/project.properties b/project.properties
index 1d35d2d..cd0ca12 100644
--- a/project.properties
+++ b/project.properties
@@ -12,4 +12,4 @@
android.library=true
# Project target.
-target=android-14
+target=android-8
diff --git a/src/com/codebutler/android_websockets/HybiParser.java b/src/com/codebutler/android_websockets/HybiParser.java
index e7a455b..e13eb8d 100644
--- a/src/com/codebutler/android_websockets/HybiParser.java
+++ b/src/com/codebutler/android_websockets/HybiParser.java
@@ -149,7 +149,7 @@ private void parseOpcode(byte data) throws ProtocolError {
throw new ProtocolError("Bad opcode");
}
- if (FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) {
+ if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) {
throw new ProtocolError("Expected non-final packet");
}
@@ -332,9 +332,42 @@ private int getInteger(byte[] bytes) throws ProtocolError {
}
return (int) i;
}
+
+ /**
+ * Copied from AOSP Arrays.java.
+ */
+ /**
+ * Copies elements from {@code original} into a new array, from indexes start (inclusive) to
+ * end (exclusive). The original order of elements is preserved.
+ * If {@code end} is greater than {@code original.length}, the result is padded
+ * with the value {@code (byte) 0}.
+ *
+ * @param original the original array
+ * @param start the start index, inclusive
+ * @param end the end index, exclusive
+ * @return the new array
+ * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
+ * @throws IllegalArgumentException if {@code start > end}
+ * @throws NullPointerException if {@code original == null}
+ * @since 1.6
+ */
+ private static byte[] copyOfRange(byte[] original, int start, int end) {
+ if (start > end) {
+ throw new IllegalArgumentException();
+ }
+ int originalLength = original.length;
+ if (start < 0 || start > originalLength) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ int resultLength = end - start;
+ int copyLength = Math.min(resultLength, originalLength - start);
+ byte[] result = new byte[resultLength];
+ System.arraycopy(original, start, result, 0, copyLength);
+ return result;
+ }
private byte[] slice(byte[] array, int start) {
- return Arrays.copyOfRange(array, start, array.length);
+ return copyOfRange(array, start, array.length);
}
public static class ProtocolError extends IOException {
diff --git a/src/com/codebutler/android_websockets/WebSocketClient.java b/src/com/codebutler/android_websockets/WebSocketClient.java
index 7e3343c..84b46b2 100644
--- a/src/com/codebutler/android_websockets/WebSocketClient.java
+++ b/src/com/codebutler/android_websockets/WebSocketClient.java
@@ -22,6 +22,7 @@
import java.net.Socket;
import java.net.URI;
import java.security.KeyManagementException;
+import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
@@ -36,6 +37,7 @@ public class WebSocketClient {
private Handler mHandler;
private List mExtraHeaders;
private HybiParser mParser;
+ private boolean mConnected;
private final Object mSendLock = new Object();
@@ -47,8 +49,9 @@ public static void setTrustManagers(TrustManager[] tm) {
public WebSocketClient(URI uri, Listener listener, List extraHeaders) {
mURI = uri;
- mListener = listener;
+ mListener = listener;
mExtraHeaders = extraHeaders;
+ mConnected = false;
mParser = new HybiParser(this);
mHandlerThread = new HandlerThread("websocket-thread");
@@ -69,7 +72,7 @@ public void connect() {
@Override
public void run() {
try {
- int port = (mURI.getPort() != -1) ? mURI.getPort() : (mURI.getScheme().equals("wss") ? 443 : 80);
+ int port = (mURI.getPort() != -1) ? mURI.getPort() : ((mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? 443 : 80);
String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath();
if (!TextUtils.isEmpty(mURI.getQuery())) {
@@ -79,16 +82,17 @@ public void run() {
String originScheme = mURI.getScheme().equals("wss") ? "https" : "http";
URI origin = new URI(originScheme, "//" + mURI.getHost(), null);
- SocketFactory factory = mURI.getScheme().equals("wss") ? getSSLSocketFactory() : SocketFactory.getDefault();
+ SocketFactory factory = (mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? getSSLSocketFactory() : SocketFactory.getDefault();
mSocket = factory.createSocket(mURI.getHost(), port);
PrintWriter out = new PrintWriter(mSocket.getOutputStream());
+ String secretKey = createSecret();
out.print("GET " + path + " HTTP/1.1\r\n");
out.print("Upgrade: websocket\r\n");
out.print("Connection: Upgrade\r\n");
out.print("Host: " + mURI.getHost() + "\r\n");
out.print("Origin: " + origin.toString() + "\r\n");
- out.print("Sec-WebSocket-Key: " + createSecret() + "\r\n");
+ out.print("Sec-WebSocket-Key: " + secretKey + "\r\n");
out.print("Sec-WebSocket-Version: 13\r\n");
if (mExtraHeaders != null) {
for (NameValuePair pair : mExtraHeaders) {
@@ -113,23 +117,32 @@ public void run() {
while (!TextUtils.isEmpty(line = readLine(stream))) {
Header header = parseHeader(line);
if (header.getName().equals("Sec-WebSocket-Accept")) {
- // FIXME: Verify the response...
+ String expected = expectedKey(secretKey);
+ if (expected == null) {
+ throw new Exception("SHA-1 algorithm not found");
+ } else if (!expected.equals(header.getValue())) {
+ throw new Exception("Invalid Sec-WebSocket-Accept, expected: " + expected + ", got: " + header.getValue());
+ }
}
}
mListener.onConnect();
+ mConnected = true;
+
// Now decode websocket frames.
mParser.start(stream);
} catch (EOFException ex) {
Log.d(TAG, "WebSocket EOF!", ex);
mListener.onDisconnect(0, "EOF");
+ mConnected = false;
} catch (SSLException ex) {
// Connection reset by peer
Log.d(TAG, "Websocket SSL error!", ex);
mListener.onDisconnect(0, "SSL");
+ mConnected = false;
} catch (Exception ex) {
mListener.onError(ex);
@@ -144,13 +157,16 @@ public void disconnect() {
mHandler.post(new Runnable() {
@Override
public void run() {
- try {
- mSocket.close();
+ if (mSocket != null) {
+ try {
+ mSocket.close();
+ } catch (IOException ex) {
+ Log.d(TAG, "Error while disconnecting", ex);
+ mListener.onError(ex);
+ }
mSocket = null;
- } catch (IOException ex) {
- Log.d(TAG, "Error while disconnecting", ex);
- mListener.onError(ex);
}
+ mConnected = false;
}
});
}
@@ -164,6 +180,10 @@ public void send(byte[] data) {
sendFrame(mParser.frame(data));
}
+ public boolean isConnected() {
+ return mConnected;
+ }
+
private StatusLine parseStatusLine(String line) {
if (TextUtils.isEmpty(line)) {
return null;
@@ -195,6 +215,19 @@ private String readLine(HybiParser.HappyDataInputStream reader) throws IOExcepti
return string.toString();
}
+ private String expectedKey(String secret) {
+ //concatenate, SHA1-hash, base64-encode
+ try {
+ final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ final String secretGUID = secret + GUID;
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ byte[] digest = md.digest(secretGUID.getBytes());
+ return Base64.encodeToString(digest, Base64.DEFAULT).trim();
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+
private String createSecret() {
byte[] nonce = new byte[16];
for (int i = 0; i < 16; i++) {
@@ -233,4 +266,4 @@ private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException,
context.init(null, sTrustManagers, null);
return context.getSocketFactory();
}
-}
+}
\ No newline at end of file
diff --git a/src/com/koushikdutta/async/http/socketio/Acknowledge.java b/src/com/koushikdutta/async/http/socketio/Acknowledge.java
new file mode 100644
index 0000000..8bf1c64
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/Acknowledge.java
@@ -0,0 +1,7 @@
+package com.koushikdutta.async.http.socketio;
+
+import org.json.JSONArray;
+
+public interface Acknowledge {
+ void acknowledge(JSONArray arguments);
+}
diff --git a/src/com/koushikdutta/async/http/socketio/ConnectCallback.java b/src/com/koushikdutta/async/http/socketio/ConnectCallback.java
new file mode 100644
index 0000000..7ff69d7
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/ConnectCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.http.socketio;
+
+public interface ConnectCallback {
+ public void onConnectCompleted(Exception ex, SocketIOClient client);
+}
\ No newline at end of file
diff --git a/src/com/koushikdutta/async/http/socketio/DisconnectCallback.java b/src/com/koushikdutta/async/http/socketio/DisconnectCallback.java
new file mode 100644
index 0000000..97ec4b6
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/DisconnectCallback.java
@@ -0,0 +1,8 @@
+package com.koushikdutta.async.http.socketio;
+
+/**
+ * Created by koush on 7/2/13.
+ */
+public interface DisconnectCallback {
+ void onDisconnect(Exception e);
+}
diff --git a/src/com/koushikdutta/async/http/socketio/ErrorCallback.java b/src/com/koushikdutta/async/http/socketio/ErrorCallback.java
new file mode 100644
index 0000000..6411667
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/ErrorCallback.java
@@ -0,0 +1,8 @@
+package com.koushikdutta.async.http.socketio;
+
+/**
+ * Created by koush on 7/2/13.
+ */
+public interface ErrorCallback {
+ void onError(String error);
+}
diff --git a/src/com/koushikdutta/async/http/socketio/EventCallback.java b/src/com/koushikdutta/async/http/socketio/EventCallback.java
new file mode 100644
index 0000000..d416b2d
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/EventCallback.java
@@ -0,0 +1,7 @@
+package com.koushikdutta.async.http.socketio;
+
+import org.json.JSONArray;
+
+public interface EventCallback {
+ public void onEvent(String event, JSONArray argument, Acknowledge acknowledge);
+}
\ No newline at end of file
diff --git a/src/com/koushikdutta/async/http/socketio/EventEmitter.java b/src/com/koushikdutta/async/http/socketio/EventEmitter.java
new file mode 100644
index 0000000..f6dbb7a
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/EventEmitter.java
@@ -0,0 +1,55 @@
+package com.koushikdutta.async.http.socketio;
+
+import com.koushikdutta.async.util.HashList;
+
+import org.json.JSONArray;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Created by koush on 7/1/13.
+ */
+public class EventEmitter {
+ interface OnceCallback extends EventCallback {
+ }
+
+ HashList callbacks = new HashList();
+
+ void onEvent(String event, JSONArray arguments, Acknowledge acknowledge) {
+ List list = callbacks.get(event);
+ if (list == null)
+ return;
+ Iterator iter = list.iterator();
+ while (iter.hasNext()) {
+ EventCallback cb = iter.next();
+ cb.onEvent(event, arguments, acknowledge);
+ if (cb instanceof OnceCallback)
+ iter.remove();
+ }
+ }
+
+ public void addListener(String event, EventCallback callback) {
+ on(event, callback);
+ }
+
+ public void once(final String event, final EventCallback callback) {
+ on(event, new OnceCallback() {
+ @Override
+ public void onEvent(String event, JSONArray arguments, Acknowledge acknowledge) {
+ callback.onEvent(event, arguments, acknowledge);
+ }
+ });
+ }
+
+ public void on(String event, EventCallback callback) {
+ callbacks.add(event, callback);
+ }
+
+ public void removeListener(String event, EventCallback callback) {
+ List list = callbacks.get(event);
+ if (list == null)
+ return;
+ list.remove(callback);
+ }
+}
diff --git a/src/com/koushikdutta/async/http/socketio/JSONCallback.java b/src/com/koushikdutta/async/http/socketio/JSONCallback.java
new file mode 100644
index 0000000..52d8aff
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/JSONCallback.java
@@ -0,0 +1,8 @@
+package com.koushikdutta.async.http.socketio;
+
+import org.json.JSONObject;
+
+public interface JSONCallback {
+ public void onJSON(JSONObject json, Acknowledge acknowledge);
+}
+
diff --git a/src/com/koushikdutta/async/http/socketio/ReconnectCallback.java b/src/com/koushikdutta/async/http/socketio/ReconnectCallback.java
new file mode 100644
index 0000000..bd427f4
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/ReconnectCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.http.socketio;
+
+public interface ReconnectCallback {
+ public void onReconnect();
+}
\ No newline at end of file
diff --git a/src/com/koushikdutta/async/http/socketio/SocketIOClient.java b/src/com/koushikdutta/async/http/socketio/SocketIOClient.java
new file mode 100644
index 0000000..af704fe
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/SocketIOClient.java
@@ -0,0 +1,184 @@
+package com.koushikdutta.async.http.socketio;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import android.os.Handler;
+import android.text.TextUtils;
+
+import com.codebutler.android_websockets.WebSocketClient;
+import com.koushikdutta.http.AsyncHttpClient;
+import com.koushikdutta.http.AsyncHttpClient.SocketIORequest;
+
+public class SocketIOClient extends EventEmitter {
+
+ boolean connected;
+ boolean disconnected;
+ Handler handler;
+
+ private void emitRaw(int type, String message, Acknowledge acknowledge) {
+ connection.emitRaw(type, this, message, acknowledge);
+ }
+
+ public void emit(String name, JSONArray args) {
+ emit(name, args, null);
+ }
+
+ public void emit(final String message) {
+ emit(message, (Acknowledge) null);
+ }
+
+ public void emit(final JSONObject jsonMessage) {
+ emit(jsonMessage, null);
+ }
+
+ public void emit(String name, JSONArray args, Acknowledge acknowledge) {
+ final JSONObject event = new JSONObject();
+ try {
+ event.put("name", name);
+ event.put("args", args);
+ emitRaw(5, event.toString(), acknowledge);
+ } catch (Exception e) {
+ }
+ }
+
+ public void emit(final String message, Acknowledge acknowledge) {
+ emitRaw(3, message, acknowledge);
+ }
+
+ public void emit(final JSONObject jsonMessage, Acknowledge acknowledge) {
+ emitRaw(4, jsonMessage.toString(), acknowledge);
+ }
+
+ public static void connect(String uri, final ConnectCallback callback, final Handler handler) {
+ connect(new SocketIORequest(uri), callback, handler);
+ }
+
+ ConnectCallback connectCallback;
+
+ public static void connect(final SocketIORequest request, final ConnectCallback callback, final Handler handler) {
+
+ final SocketIOConnection connection = new SocketIOConnection(handler, new AsyncHttpClient(), request);
+
+ final ConnectCallback wrappedCallback = new ConnectCallback() {
+ @Override
+ public void onConnectCompleted(Exception ex, SocketIOClient client) {
+ if (ex != null || TextUtils.isEmpty(request.getEndpoint())) {
+
+ client.handler = handler;
+ if (callback != null) {
+ callback.onConnectCompleted(ex, client);
+ }
+
+ return;
+ }
+
+ // remove the root client since that's not actually being used.
+ connection.clients.remove(client);
+
+ // connect to the endpoint we want
+ client.of(request.getEndpoint(), new ConnectCallback() {
+ @Override
+ public void onConnectCompleted(Exception ex, SocketIOClient client) {
+ if (callback != null) {
+ callback.onConnectCompleted(ex, client);
+ }
+ }
+ });
+ }
+ };
+
+ connection.clients.add(new SocketIOClient(connection, "", wrappedCallback));
+ connection.reconnect();
+
+ }
+
+ ErrorCallback errorCallback;
+
+ public void setErrorCallback(ErrorCallback callback) {
+ errorCallback = callback;
+ }
+
+ public ErrorCallback getErrorCallback() {
+ return errorCallback;
+ }
+
+ DisconnectCallback disconnectCallback;
+
+ public void setDisconnectCallback(DisconnectCallback callback) {
+ disconnectCallback = callback;
+ }
+
+ public DisconnectCallback getDisconnectCallback() {
+ return disconnectCallback;
+ }
+
+ ReconnectCallback reconnectCallback;
+
+ public void setReconnectCallback(ReconnectCallback callback) {
+ reconnectCallback = callback;
+ }
+
+ public ReconnectCallback getReconnectCallback() {
+ return reconnectCallback;
+ }
+
+ JSONCallback jsonCallback;
+
+ public void setJSONCallback(JSONCallback callback) {
+ jsonCallback = callback;
+ }
+
+ public JSONCallback getJSONCallback() {
+ return jsonCallback;
+ }
+
+ StringCallback stringCallback;
+
+ public void setStringCallback(StringCallback callback) {
+ stringCallback = callback;
+ }
+
+ public StringCallback getStringCallback() {
+ return stringCallback;
+ }
+
+ SocketIOConnection connection;
+ String endpoint;
+
+ private SocketIOClient(SocketIOConnection connection, String endpoint,
+ ConnectCallback callback) {
+ this.endpoint = endpoint;
+ this.connection = connection;
+ this.connectCallback = callback;
+ }
+
+ public boolean isConnected() {
+ return connected && !disconnected && connection.isConnected();
+ }
+
+ public void disconnect() {
+ connection.disconnect(this);
+ final DisconnectCallback disconnectCallback = this.disconnectCallback;
+ if (disconnectCallback != null) {
+ handler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ disconnectCallback.onDisconnect(null);
+
+ }
+ });
+
+ }
+ }
+
+ public void of(String endpoint, ConnectCallback connectCallback) {
+ connection.connect(new SocketIOClient(connection, endpoint, connectCallback));
+ }
+
+ public WebSocketClient getWebSocket() {
+ return connection.webSocketClient;
+ }
+
+}
diff --git a/src/com/koushikdutta/async/http/socketio/SocketIOConnection.java b/src/com/koushikdutta/async/http/socketio/SocketIOConnection.java
new file mode 100644
index 0000000..f8efcdd
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/SocketIOConnection.java
@@ -0,0 +1,445 @@
+package com.koushikdutta.async.http.socketio;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Hashtable;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.text.TextUtils;
+
+import com.codebutler.android_websockets.WebSocketClient;
+import com.codebutler.android_websockets.WebSocketClient.Listener;
+import com.koushikdutta.http.AsyncHttpClient;
+import com.koushikdutta.http.AsyncHttpClient.SocketIORequest;
+
+/**
+ * Created by koush on 7/1/13.
+ */
+class SocketIOConnection {
+
+ private Handler mHandler;
+ AsyncHttpClient httpClient;
+ int heartbeat;
+ ArrayList clients = new ArrayList();
+ WebSocketClient webSocketClient;
+ SocketIORequest request;
+
+ public SocketIOConnection(Handler handler, AsyncHttpClient httpClient,
+ SocketIORequest request) {
+ mHandler = handler;
+ this.httpClient = httpClient;
+ this.request = request;
+ }
+
+ public boolean isConnected() {
+ return webSocketClient != null && webSocketClient.isConnected();
+ }
+
+ Hashtable acknowledges = new Hashtable();
+ int ackCount;
+
+ public void emitRaw(int type, SocketIOClient client, String message, Acknowledge acknowledge) {
+ String ack = "";
+ if (acknowledge != null) {
+ String id = "" + ackCount++;
+ ack = id + "+";
+ acknowledges.put(id, acknowledge);
+ }
+ webSocketClient.send(String.format("%d:%s:%s:%s", type, ack, client.endpoint, message));
+ }
+
+ public void connect(SocketIOClient client) {
+ clients.add(client);
+ webSocketClient.send(String.format("1::%s", client.endpoint));
+ }
+
+ public void disconnect(SocketIOClient client) {
+ clients.remove(client);
+
+ // see if we can leave this endpoint completely
+ boolean needsEndpointDisconnect = true;
+ for (SocketIOClient other : clients) {
+ // if this is the default endpoint (which disconnects everything),
+ // or another client is using this endpoint,
+ // we can't disconnect
+ if (TextUtils.equals(other.endpoint, client.endpoint)
+ || TextUtils.isEmpty(client.endpoint)) {
+ needsEndpointDisconnect = false;
+ break;
+ }
+ }
+
+ if (needsEndpointDisconnect)
+ webSocketClient.send(String.format("0::%s", client.endpoint));
+
+ // and see if we can disconnect the socket completely
+ if (clients.size() > 0)
+ return;
+
+ webSocketClient.disconnect();
+ webSocketClient = null;
+ }
+
+ void reconnect() {
+ if (isConnected()) {
+ return;
+ }
+
+ // initiate a session
+ httpClient.executeString(request, new AsyncHttpClient.StringCallback() {
+ @Override
+ public void onCompleted(final Exception e, String result) {
+ if (e != null) {
+ reportDisconnect(e);
+ return;
+ }
+
+ try {
+ String[] parts = result.split(":");
+ String session = parts[0];
+ if (!"".equals(parts[1]))
+ heartbeat = Integer.parseInt(parts[1]) / 2 * 1000;
+ else
+ heartbeat = 0;
+
+ String transportsLine = parts[3];
+ String[] transports = transportsLine.split(",");
+ HashSet set = new HashSet(Arrays.asList(transports));
+ if (!set.contains("websocket"))
+ throw new Exception("websocket not supported");
+
+ final String sessionUrl = Uri.parse(request.getUri()).buildUpon()
+ .appendPath("websocket").appendPath(session)
+ .build().toString();
+
+ SocketIOConnection.this.webSocketClient = new WebSocketClient(URI.create(sessionUrl), new Listener() {
+
+ @Override
+ public void onMessage(byte[] data) {
+ //Do nothing
+
+ }
+
+ @Override
+ public void onMessage(String message) {
+ try {
+ // Log.d(TAG, "Message: " + message);
+ String[] parts = message.split(":", 4);
+ int code = Integer.parseInt(parts[0]);
+ switch (code) {
+ case 0:
+ // disconnect
+ webSocketClient.disconnect();
+ reportDisconnect(null);
+ break;
+ case 1:
+ // connect
+ reportConnect(parts[2]);
+ break;
+ case 2:
+ // heartbeat
+ webSocketClient.send("2::");
+ break;
+ case 3: {
+ // message
+ reportString(parts[2], parts[3], acknowledge(parts[1]));
+ break;
+ }
+ case 4: {
+ // json message
+ final String dataString = parts[3];
+ final JSONObject jsonMessage = new JSONObject(dataString);
+ reportJson(parts[2], jsonMessage, acknowledge(parts[1]));
+ break;
+ }
+ case 5: {
+ final String dataString = parts[3];
+ final JSONObject data = new JSONObject(dataString);
+ final String event = data.getString("name");
+ final JSONArray args = data.optJSONArray("args");
+ reportEvent(parts[2], event, args, acknowledge(parts[1]));
+ break;
+ }
+ case 6:
+ // ACK
+ final String[] ackParts = parts[3].split("\\+", 2);
+ Acknowledge ack = acknowledges.remove(ackParts[0]);
+ if (ack == null)
+ return;
+ JSONArray arguments = null;
+ if (ackParts.length == 2)
+ arguments = new JSONArray(ackParts[1]);
+ ack.acknowledge(arguments);
+ break;
+ case 7:
+ // error
+ reportError(parts[2], parts[3]);
+ break;
+ case 8:
+ // noop
+ break;
+ default:
+ throw new Exception("unknown code");
+ }
+ } catch (Exception ex) {
+ webSocketClient.disconnect();
+ webSocketClient = null;
+ reportDisconnect(ex);
+ }
+
+
+ }
+
+ @Override
+ public void onError(Exception error) {
+ reportDisconnect(error);
+ }
+
+ @Override
+ public void onDisconnect(int code, String reason) {
+
+ reportDisconnect(new IOException(String.format("Disconnected code %d for reason %s", code, reason)));
+ }
+
+ @Override
+ public void onConnect() {
+ reconnectDelay = 1000L;
+ setupHeartbeat();
+
+ }
+ }, null);
+ SocketIOConnection.this.webSocketClient.connect();
+
+ } catch (Exception ex) {
+ reportDisconnect(ex);
+ }
+ }
+ });
+
+ }
+
+ void setupHeartbeat() {
+ final WebSocketClient ws = webSocketClient;
+ Runnable heartbeatRunner = new Runnable() {
+ @Override
+ public void run() {
+ if (heartbeat <= 0 || ws != webSocketClient || ws == null
+ || !ws.isConnected())
+ return;
+ webSocketClient.send("2:::");
+
+ mHandler.postDelayed(this, heartbeat);
+ }
+ };
+ heartbeatRunner.run();
+ }
+
+ private interface SelectCallback {
+ void onSelect(SocketIOClient client);
+ }
+
+ private void select(String endpoint, SelectCallback callback) {
+ for (SocketIOClient client : clients) {
+ if (endpoint == null || TextUtils.equals(client.endpoint, endpoint)) {
+ callback.onSelect(client);
+ }
+ }
+ }
+
+ private void delayReconnect() {
+ if (webSocketClient != null || clients.size() == 0)
+ return;
+
+ // see if any client has disconnected,
+ // and that we need a reconnect
+ boolean disconnected = false;
+ for (SocketIOClient client : clients) {
+ if (client.disconnected) {
+ disconnected = true;
+ break;
+ }
+ }
+
+ if (!disconnected)
+ return;
+
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ reconnect();
+ }
+ }, reconnectDelay);
+ reconnectDelay *= 2;
+ }
+
+ long reconnectDelay = 1000L;
+
+ private void reportDisconnect(final Exception ex) {
+ select(null, new SelectCallback() {
+ @Override
+ public void onSelect(final SocketIOClient client) {
+ if (client.connected) {
+ client.disconnected = true;
+ final DisconnectCallback closed = client.getDisconnectCallback();
+ if (closed != null) {
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ closed.onDisconnect(ex);
+
+ }
+ });
+
+ }
+ } else {
+ // client has never connected, this is a initial connect
+ // failure
+ final ConnectCallback callback = client.connectCallback;
+ if (callback != null) {
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onConnectCompleted(ex, client);
+
+ }
+ });
+
+ }
+ }
+ }
+ });
+
+ delayReconnect();
+ }
+
+ private void reportConnect(String endpoint) {
+ select(endpoint, new SelectCallback() {
+ @Override
+ public void onSelect(SocketIOClient client) {
+ if (client.isConnected())
+ return;
+ if (!client.connected) {
+ // normal connect
+ client.connected = true;
+ ConnectCallback callback = client.connectCallback;
+ if (callback != null)
+ callback.onConnectCompleted(null, client);
+ } else if (client.disconnected) {
+ // reconnect
+ client.disconnected = false;
+ ReconnectCallback callback = client.reconnectCallback;
+ if (callback != null)
+ callback.onReconnect();
+ } else {
+ // double connect?
+ // assert false;
+ }
+ }
+ });
+ }
+
+ private void reportJson(String endpoint, final JSONObject jsonMessage, final Acknowledge acknowledge) {
+ select(endpoint, new SelectCallback() {
+ @Override
+ public void onSelect(SocketIOClient client) {
+ final JSONCallback callback = client.jsonCallback;
+ if (callback != null) {
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onJSON(jsonMessage, acknowledge);
+
+ }
+ });
+ }
+
+ }
+ });
+ }
+
+ private void reportString(String endpoint, final String string, final Acknowledge acknowledge) {
+ select(endpoint, new SelectCallback() {
+ @Override
+ public void onSelect(SocketIOClient client) {
+ final StringCallback callback = client.stringCallback;
+ if (callback != null) {
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onString(string, acknowledge);
+
+ }
+ });
+
+ }
+ }
+ });
+ }
+
+ private void reportEvent(String endpoint, final String event, final JSONArray arguments, final Acknowledge acknowledge) {
+ select(endpoint, new SelectCallback() {
+ @Override
+ public void onSelect(final SocketIOClient client) {
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ client.onEvent(event, arguments, acknowledge);
+
+ }
+ });
+
+ }
+ });
+ }
+
+ private void reportError(String endpoint, final String error) {
+ select(endpoint, new SelectCallback() {
+ @Override
+ public void onSelect(SocketIOClient client) {
+ final ErrorCallback callback = client.errorCallback;
+ if (callback != null) {
+
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onError(error);
+
+ }
+ });
+
+ }
+ }
+ });
+ }
+
+ private Acknowledge acknowledge(final String messageId) {
+ if (TextUtils.isEmpty(messageId))
+ return null;
+
+ return new Acknowledge() {
+ @Override
+ public void acknowledge(JSONArray arguments) {
+ String data = "";
+ if (arguments != null)
+ data += "+" + arguments.toString();
+ webSocketClient.send(String.format("6:::%s%s", messageId, data));
+ }
+ };
+ }
+
+
+
+}
diff --git a/src/com/koushikdutta/async/http/socketio/StringCallback.java b/src/com/koushikdutta/async/http/socketio/StringCallback.java
new file mode 100644
index 0000000..7062b0f
--- /dev/null
+++ b/src/com/koushikdutta/async/http/socketio/StringCallback.java
@@ -0,0 +1,5 @@
+package com.koushikdutta.async.http.socketio;
+
+public interface StringCallback {
+ public void onString(String string, Acknowledge acknowledge);
+}
\ No newline at end of file
diff --git a/src/com/koushikdutta/async/util/HashList.java b/src/com/koushikdutta/async/util/HashList.java
new file mode 100644
index 0000000..0036661
--- /dev/null
+++ b/src/com/koushikdutta/async/util/HashList.java
@@ -0,0 +1,29 @@
+package com.koushikdutta.async.util;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+
+/**
+ * Created by koush on 5/27/13.
+ */
+public class HashList extends Hashtable> {
+
+ private static final long serialVersionUID = 1L;
+
+ public HashList() {
+ }
+
+ public boolean contains(String key) {
+ ArrayList check = get(key);
+ return check != null && check.size() > 0;
+ }
+
+ public void add(String key, T value) {
+ ArrayList ret = get(key);
+ if (ret == null) {
+ ret = new ArrayList();
+ put(key, ret);
+ }
+ ret.add(value);
+ }
+}
diff --git a/src/com/koushikdutta/http/AsyncHttpClient.java b/src/com/koushikdutta/http/AsyncHttpClient.java
new file mode 100644
index 0000000..c1855a5
--- /dev/null
+++ b/src/com/koushikdutta/http/AsyncHttpClient.java
@@ -0,0 +1,133 @@
+package com.koushikdutta.http;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.message.BasicNameValuePair;
+
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.AsyncTask;
+
+import com.codebutler.android_websockets.WebSocketClient;
+
+/**
+ *
+ * Created by Vinay S Shenoy on 07/09/2013
+ */
+public class AsyncHttpClient {
+
+ public AsyncHttpClient() {
+
+ }
+
+ public static class SocketIORequest {
+
+ private String mUri;
+ private String mEndpoint;
+ private List mHeaders;
+
+ public SocketIORequest(String uri) {
+ this(uri, null);
+ }
+
+ public SocketIORequest(String uri, String endpoint) {
+ this(uri, endpoint, null);
+ }
+
+ public SocketIORequest(String uri, String endpoint, List headers) {
+ mUri = Uri.parse(uri).buildUpon().encodedPath("/socket.io/1/").build().toString();
+ mEndpoint = endpoint;
+ mHeaders = headers;
+ }
+
+ public String getUri() {
+ return mUri;
+ }
+
+ public String getEndpoint() {
+ return mEndpoint;
+ }
+
+ public List getHeaders() {
+ return mHeaders;
+ }
+ }
+
+ public static interface StringCallback {
+ public void onCompleted(final Exception e, String result);
+ }
+
+ public static interface WebSocketConnectCallback {
+ public void onCompleted(Exception ex, WebSocketClient webSocket);
+ }
+
+ public void executeString(final SocketIORequest socketIORequest, final StringCallback stringCallback) {
+
+ new AsyncTask() {
+
+ @Override
+ protected Void doInBackground(Void... params) {
+
+ AndroidHttpClient httpClient = AndroidHttpClient.newInstance("android-websockets-2.0");
+ HttpPost post = new HttpPost(socketIORequest.getUri());
+ addHeadersToRequest(post, socketIORequest.getHeaders());
+
+ try {
+ HttpResponse res = httpClient.execute(post);
+ String responseString = readToEnd(res.getEntity().getContent());
+
+ if (stringCallback != null) {
+ stringCallback.onCompleted(null, responseString);
+ }
+
+ } catch (IOException e) {
+
+ if (stringCallback != null) {
+ stringCallback.onCompleted(e, null);
+ }
+ } finally {
+ httpClient.close();
+ httpClient = null;
+ }
+ return null;
+ }
+
+ private void addHeadersToRequest(HttpRequest request, List headers) {
+ if (headers != null) {
+ Iterator it = headers.iterator();
+ while (it.hasNext()) {
+ BasicNameValuePair header = it.next();
+ request.addHeader(header.getName(), header.getValue());
+ }
+ }
+ }
+ }.execute();
+ }
+
+ private byte[] readToEndAsArray(InputStream input) throws IOException {
+ DataInputStream dis = new DataInputStream(input);
+ byte[] stuff = new byte[1024];
+ ByteArrayOutputStream buff = new ByteArrayOutputStream();
+ int read = 0;
+ while ((read = dis.read(stuff)) != -1) {
+ buff.write(stuff, 0, read);
+ }
+
+ return buff.toByteArray();
+ }
+
+ private String readToEnd(InputStream input) throws IOException {
+ return new String(readToEndAsArray(input));
+ }
+
+}