diff --git a/.gitignore b/.gitignore
index 81d81b4..926c55d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
-gen/
+bin
+gen
+.classpath
+.project
local.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..0667347 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,18 @@
-# WebSocket client for Android
+# WebSocket and Socket.IO client for Android
-A very simple bare-minimum WebSocket client for Android.
+A very simple bare-minimum 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).
+
+## WebSocket Usage
```java
List extraHeaders = Arrays.asList(
@@ -52,6 +54,59 @@ client.send(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF });
client.disconnect();
```
+## Socket.IO Usage
+
+```java
+SocketIOClient client = new SocketIOClient(URI.create("wss://example.com"), new SocketIOClient.Handler() {
+ @Override
+ public void onConnect() {
+ Log.d(TAG, "Connected!");
+ }
+
+ @Override
+ public void on(String event, JSONArray arguments) {
+ Log.d(TAG, String.format("Got event %s: %s", event, arguments.toString()));
+ }
+
+ @Override
+ public void onJSON(JSONObject json) {
+ try {
+ Log.d(TAG, String.format("Got JSON Object: %s", json.toString()));
+ } catch(JSONException e) {
+ }
+ }
+
+ @Override
+ public void onMessage(String message) {
+ Log.d(TAG, String.format("Got message: %s", message));
+ }
+
+ @Override
+ public void onDisconnect(int code, String reason) {
+ Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason));
+ }
+
+ @Override
+ public void onError(Exception error) {
+ Log.e(TAG, "Error!", error);
+ }
+});
+
+client.connect();
+
+// Later…
+client.emit("Message"); //Message
+JSONArray arguments = new JSONArray();
+arguments.put("first argument");
+JSONObject second = new JSONObject();
+second.put("dictionary", true);
+client.emit(second); //JSON Message
+arguments.put(second);
+client.emit("hello", arguments); //Event
+client.disconnect();
+```
+
+
## TODO
@@ -64,6 +119,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..1f83f2a 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 {
@@ -380,4 +413,4 @@ public byte[] readBytes(int length) throws IOException {
return buffer;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/codebutler/android_websockets/SocketIOClient.java b/src/com/codebutler/android_websockets/SocketIOClient.java
new file mode 100644
index 0000000..21e2593
--- /dev/null
+++ b/src/com/codebutler/android_websockets/SocketIOClient.java
@@ -0,0 +1,295 @@
+package com.codebutler.android_websockets;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.net.http.AndroidHttpClient;
+import android.os.Looper;
+import android.util.Log;
+
+public class SocketIOClient {
+ public static interface Handler {
+ public void onConnect();
+
+ public void on(String event, JSONArray arguments);
+
+ public void onDisconnect(int code, String reason);
+
+ public void onJSON(JSONObject json);
+
+ public void onMessage(String message);
+
+ public void onError(Exception error);
+ }
+
+ private static final String TAG = "SocketIOClient";
+
+ String mURL;
+ Handler mHandler;
+ String mSession;
+ int mHeartbeat;
+ WebSocketClient mClient;
+
+ public SocketIOClient(URI uri, Handler handler) {
+ // remove trailing "/" from URI, in case user provided e.g. http://test.com/
+ mURL = uri.toString().replaceAll("/$", "") + "/socket.io/1/";
+ mHandler = handler;
+ }
+
+ private static String downloadUriAsString(final HttpUriRequest req) throws IOException {
+ AndroidHttpClient client = AndroidHttpClient.newInstance("android-websockets");
+ try {
+ HttpResponse res = client.execute(req);
+ return readToEnd(res.getEntity().getContent());
+ }
+ finally {
+ client.close();
+ }
+ }
+
+ private static 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 static String readToEnd(InputStream input) throws IOException {
+ return new String(readToEndAsArray(input));
+ }
+
+ android.os.Handler mSendHandler;
+ Looper mSendLooper;
+
+ public void emit(String name, JSONArray args) throws JSONException {
+ final JSONObject event = new JSONObject();
+ event.put("name", name);
+ event.put("args", args);
+ Log.d(TAG, "Emitting event: " + event.toString());
+ mSendHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mClient.send(String.format("5:::%s", event.toString()));
+ }
+ });
+ }
+
+ public void emit(final String message) {
+ mSendHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mClient.send(String.format("3:::%s", message));
+ }
+ });
+ }
+
+ public void emit(final JSONObject jsonMessage) {
+
+ mSendHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mClient.send(String.format("4:::%s", jsonMessage.toString()));
+ }
+ });
+ }
+
+ private void connectSession() throws URISyntaxException {
+ mClient = new WebSocketClient(new URI(mURL + "websocket/" + mSession), new WebSocketClient.Listener() {
+ @Override
+ public void onMessage(byte[] data) {
+ cleanup();
+ mHandler.onError(new Exception("Unexpected binary data"));
+ }
+
+ @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 1:
+ // connect
+ mHandler.onConnect();
+ break;
+ case 2:
+ // heartbeat
+ mClient.send("2::");
+ break;
+ case 3: {
+ // message
+ final String messageId = parts[1];
+ final String dataString = parts[3];
+
+ if(!"".equals(messageId)) {
+ mSendHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mClient.send(String.format("6:::%s", messageId));
+ }
+ });
+ }
+ mHandler.onMessage(dataString);
+ break;
+ }
+ case 4: {
+ //json message
+ final String messageId = parts[1];
+ final String dataString = parts[3];
+
+ JSONObject jsonMessage = null;
+
+ try {
+ jsonMessage = new JSONObject(dataString);
+ } catch(JSONException e) {
+ jsonMessage = new JSONObject();
+ }
+ if(!"".equals(messageId)) {
+ mSendHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ mClient.send(String.format("6:::%s", messageId));
+ }
+ });
+ }
+ mHandler.onJSON(jsonMessage);
+ break;
+ }
+ case 5: {
+ final String messageId = parts[1];
+ final String dataString = parts[3];
+ JSONObject data = new JSONObject(dataString);
+ String event = data.getString("name");
+ JSONArray args;
+ try {
+ args = data.getJSONArray("args");
+ } catch (JSONException e) {
+ args = new JSONArray();
+ }
+ if (!"".equals(messageId)) {
+ mSendHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mClient.send(String.format("6:::%s", messageId));
+ }
+ });
+ }
+ mHandler.on(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) {
+ cleanup();
+ onError(ex);
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ cleanup();
+ mHandler.onError(error);
+ }
+
+ @Override
+ public void onDisconnect(int code, String reason) {
+ cleanup();
+ // attempt reconnect with same session?
+ mHandler.onDisconnect(code, reason);
+ }
+
+ @Override
+ public void onConnect() {
+ mSendHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mSendHandler.postDelayed(this, mHeartbeat);
+ mClient.send("2:::");
+ }
+ }, mHeartbeat);
+ }
+ }, null);
+ mClient.connect();
+ }
+
+ public void disconnect() throws IOException {
+ cleanup();
+ }
+
+ private void cleanup() {
+ mClient.disconnect();
+ mClient = null;
+
+ mSendLooper.quit();
+ mSendLooper = null;
+ mSendHandler = null;
+ }
+
+ public void connect() {
+ if (mClient != null)
+ return;
+ new Thread() {
+ public void run() {
+ HttpPost post = new HttpPost(mURL);
+ try {
+ String line = downloadUriAsString(post);
+ String[] parts = line.split(":");
+ mSession = parts[0];
+ String heartbeat = parts[1];
+ if (!"".equals(heartbeat))
+ mHeartbeat = Integer.parseInt(heartbeat) / 2 * 1000;
+ 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");
+
+ Looper.prepare();
+ mSendLooper = Looper.myLooper();
+ mSendHandler = new android.os.Handler();
+
+ connectSession();
+
+ Looper.loop();
+ }
+ catch (Exception e) {
+ mHandler.onError(e);
+ }
+ };
+ }.start();
+ }
+}
+
diff --git a/src/com/codebutler/android_websockets/WebSocketClient.java b/src/com/codebutler/android_websockets/WebSocketClient.java
index 7e3343c..82aadbe 100644
--- a/src/com/codebutler/android_websockets/WebSocketClient.java
+++ b/src/com/codebutler/android_websockets/WebSocketClient.java
@@ -36,6 +36,7 @@ public class WebSocketClient {
private Handler mHandler;
private List mExtraHeaders;
private HybiParser mParser;
+ private boolean mConnected;
private final Object mSendLock = new Object();
@@ -47,8 +48,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 +71,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,7 +81,7 @@ 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());
@@ -119,17 +121,21 @@ public void run() {
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);
@@ -147,6 +153,7 @@ public void run() {
try {
mSocket.close();
mSocket = null;
+ mConnected = false;
} catch (IOException ex) {
Log.d(TAG, "Error while disconnecting", ex);
mListener.onError(ex);
@@ -164,6 +171,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;