Repository: koush/android-websockets
Branch: master
Commit: d18376158a78
Files: 22
Total size: 64.7 KB
Directory structure:
gitextract_fsf22rvj/
├── .gitignore
├── AndroidManifest.xml
├── README.md
├── android-websockets.iml
├── ant.properties
├── build.xml
├── proguard-project.txt
└── src/
└── com/
├── codebutler/
│ └── android_websockets/
│ ├── HybiParser.java
│ └── WebSocketClient.java
└── koushikdutta/
├── async/
│ ├── http/
│ │ └── socketio/
│ │ ├── Acknowledge.java
│ │ ├── ConnectCallback.java
│ │ ├── DisconnectCallback.java
│ │ ├── ErrorCallback.java
│ │ ├── EventCallback.java
│ │ ├── EventEmitter.java
│ │ ├── JSONCallback.java
│ │ ├── ReconnectCallback.java
│ │ ├── SocketIOClient.java
│ │ ├── SocketIOConnection.java
│ │ └── StringCallback.java
│ └── util/
│ └── HashList.java
└── http/
└── AsyncHttpClient.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
bin
gen
.classpath
.project
local.properties
/.settings
project.properties
================================================
FILE: AndroidManifest.xml
================================================
================================================
FILE: README.md
================================================
# THIS LIBRARY IS DEPRECATED IN FAVOR OF:
[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!
The hybi parser was ported from JavaScript to Java by [Eric Butler](https://twitter.com/codebutler) .
The WebSocket client was written by [Eric Butler](https://twitter.com/codebutler) .
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")
);
WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Listener() {
@Override
public void onConnect() {
Log.d(TAG, "Connected!");
}
@Override
public void onMessage(String message) {
Log.d(TAG, String.format("Got string message! %s", message));
}
@Override
public void onMessage(byte[] data) {
Log.d(TAG, String.format("Got binary message! %s", toHexString(data)));
}
@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);
}
}, extraHeaders);
client.connect();
// Later…
client.send("hello!");
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
* Run [autobahn tests](http://autobahn.ws/testsuite)
* Investigate using [naga](http://code.google.com/p/naga/) instead of threads.
## License
(The MIT License)
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
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.
================================================
FILE: android-websockets.iml
================================================
/res-overlay
================================================
FILE: ant.properties
================================================
# This file is used to override default values used by the Ant build system.
#
# This file must be checked into Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
================================================
FILE: build.xml
================================================
================================================
FILE: proguard-project.txt
================================================
# 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 *;
#}
================================================
FILE: src/com/codebutler/android_websockets/HybiParser.java
================================================
//
// HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser
//
// Based on code from the faye project.
// https://github.com/faye/faye-websocket-node
// Copyright (c) 2009-2012 James Coglan
//
// Ported from Javascript to Java by Eric Butler
//
// (The MIT License)
//
// 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.
package com.codebutler.android_websockets;
import android.util.Log;
import java.io.*;
import java.util.Arrays;
import java.util.List;
public class HybiParser {
private static final String TAG = "HybiParser";
private WebSocketClient mClient;
private boolean mMasking = true;
private int mStage;
private boolean mFinal;
private boolean mMasked;
private int mOpcode;
private int mLengthSize;
private int mLength;
private int mMode;
private byte[] mMask = new byte[0];
private byte[] mPayload = new byte[0];
private boolean mClosed = false;
private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();
private static final int BYTE = 255;
private static final int FIN = 128;
private static final int MASK = 128;
private static final int RSV1 = 64;
private static final int RSV2 = 32;
private static final int RSV3 = 16;
private static final int OPCODE = 15;
private static final int LENGTH = 127;
private static final int MODE_TEXT = 1;
private static final int MODE_BINARY = 2;
private static final int OP_CONTINUATION = 0;
private static final int OP_TEXT = 1;
private static final int OP_BINARY = 2;
private static final int OP_CLOSE = 8;
private static final int OP_PING = 9;
private static final int OP_PONG = 10;
private static final List OPCODES = Arrays.asList(
OP_CONTINUATION,
OP_TEXT,
OP_BINARY,
OP_CLOSE,
OP_PING,
OP_PONG
);
private static final List FRAGMENTED_OPCODES = Arrays.asList(
OP_CONTINUATION, OP_TEXT, OP_BINARY
);
public HybiParser(WebSocketClient client) {
mClient = client;
}
private static byte[] mask(byte[] payload, byte[] mask, int offset) {
if (mask.length == 0) return payload;
for (int i = 0; i < payload.length - offset; i++) {
payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]);
}
return payload;
}
public void start(HappyDataInputStream stream) throws IOException {
while (true) {
if (stream.available() == -1) break;
switch (mStage) {
case 0:
parseOpcode(stream.readByte());
break;
case 1:
parseLength(stream.readByte());
break;
case 2:
parseExtendedLength(stream.readBytes(mLengthSize));
break;
case 3:
mMask = stream.readBytes(4);
mStage = 4;
break;
case 4:
mPayload = stream.readBytes(mLength);
emitFrame();
mStage = 0;
break;
}
}
mClient.getListener().onDisconnect(0, "EOF");
}
private void parseOpcode(byte data) throws ProtocolError {
boolean rsv1 = (data & RSV1) == RSV1;
boolean rsv2 = (data & RSV2) == RSV2;
boolean rsv3 = (data & RSV3) == RSV3;
if (rsv1 || rsv2 || rsv3) {
throw new ProtocolError("RSV not zero");
}
mFinal = (data & FIN) == FIN;
mOpcode = (data & OPCODE);
mMask = new byte[0];
mPayload = new byte[0];
if (!OPCODES.contains(mOpcode)) {
throw new ProtocolError("Bad opcode");
}
if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) {
throw new ProtocolError("Expected non-final packet");
}
mStage = 1;
}
private void parseLength(byte data) {
mMasked = (data & MASK) == MASK;
mLength = (data & LENGTH);
if (mLength >= 0 && mLength <= 125) {
mStage = mMasked ? 3 : 4;
} else {
mLengthSize = (mLength == 126) ? 2 : 8;
mStage = 2;
}
}
private void parseExtendedLength(byte[] buffer) throws ProtocolError {
mLength = getInteger(buffer);
mStage = mMasked ? 3 : 4;
}
public byte[] frame(String data) {
return frame(data, OP_TEXT, -1);
}
public byte[] frame(byte[] data) {
return frame(data, OP_BINARY, -1);
}
private byte[] frame(byte[] data, int opcode, int errorCode) {
return frame((Object)data, opcode, errorCode);
}
private byte[] frame(String data, int opcode, int errorCode) {
return frame((Object)data, opcode, errorCode);
}
private byte[] frame(Object data, int opcode, int errorCode) {
if (mClosed) return null;
Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode);
byte[] buffer = (data instanceof String) ? decode((String) data) : (byte[]) data;
int insert = (errorCode > 0) ? 2 : 0;
int length = buffer.length + insert;
int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10);
int offset = header + (mMasking ? 4 : 0);
int masked = mMasking ? MASK : 0;
byte[] frame = new byte[length + offset];
frame[0] = (byte) ((byte)FIN | (byte)opcode);
if (length <= 125) {
frame[1] = (byte) (masked | length);
} else if (length <= 65535) {
frame[1] = (byte) (masked | 126);
frame[2] = (byte) Math.floor(length / 256);
frame[3] = (byte) (length & BYTE);
} else {
frame[1] = (byte) (masked | 127);
frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE);
frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE);
frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE);
frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE);
frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE);
frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE);
frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE);
frame[9] = (byte) (length & BYTE);
}
if (errorCode > 0) {
frame[offset] = (byte) (((int) Math.floor(errorCode / 256)) & BYTE);
frame[offset+1] = (byte) (errorCode & BYTE);
}
System.arraycopy(buffer, 0, frame, offset + insert, buffer.length);
if (mMasking) {
byte[] mask = {
(byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256),
(byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256)
};
System.arraycopy(mask, 0, frame, header, mask.length);
mask(frame, mask, offset);
}
return frame;
}
public void ping(String message) {
mClient.send(frame(message, OP_PING, -1));
}
public void close(int code, String reason) {
if (mClosed) return;
mClient.send(frame(reason, OP_CLOSE, code));
mClosed = true;
}
private void emitFrame() throws IOException {
byte[] payload = mask(mPayload, mMask, 0);
int opcode = mOpcode;
if (opcode == OP_CONTINUATION) {
if (mMode == 0) {
throw new ProtocolError("Mode was not set.");
}
mBuffer.write(payload);
if (mFinal) {
byte[] message = mBuffer.toByteArray();
if (mMode == MODE_TEXT) {
mClient.getListener().onMessage(encode(message));
} else {
mClient.getListener().onMessage(message);
}
reset();
}
} else if (opcode == OP_TEXT) {
if (mFinal) {
String messageText = encode(payload);
mClient.getListener().onMessage(messageText);
} else {
mMode = MODE_TEXT;
mBuffer.write(payload);
}
} else if (opcode == OP_BINARY) {
if (mFinal) {
mClient.getListener().onMessage(payload);
} else {
mMode = MODE_BINARY;
mBuffer.write(payload);
}
} else if (opcode == OP_CLOSE) {
int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0;
String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null;
Log.d(TAG, "Got close op! " + code + " " + reason);
mClient.getListener().onDisconnect(code, reason);
} else if (opcode == OP_PING) {
if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); }
Log.d(TAG, "Sending pong!!");
mClient.sendFrame(frame(payload, OP_PONG, -1));
} else if (opcode == OP_PONG) {
String message = encode(payload);
// FIXME: Fire callback...
Log.d(TAG, "Got pong! " + message);
}
}
private void reset() {
mMode = 0;
mBuffer.reset();
}
private String encode(byte[] buffer) {
try {
return new String(buffer, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private byte[] decode(String string) {
try {
return (string).getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private int getInteger(byte[] bytes) throws ProtocolError {
long i = byteArrayToLong(bytes, 0, bytes.length);
if (i < 0 || i > Integer.MAX_VALUE) {
throw new ProtocolError("Bad integer: " + i);
}
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 copyOfRange(array, start, array.length);
}
public static class ProtocolError extends IOException {
public ProtocolError(String detailMessage) {
super(detailMessage);
}
}
private static long byteArrayToLong(byte[] b, int offset, int length) {
if (b.length < length)
throw new IllegalArgumentException("length must be less than or equal to b.length");
long value = 0;
for (int i = 0; i < length; i++) {
int shift = (length - 1 - i) * 8;
value += (b[i + offset] & 0x000000FF) << shift;
}
return value;
}
public static class HappyDataInputStream extends DataInputStream {
public HappyDataInputStream(InputStream in) {
super(in);
}
public byte[] readBytes(int length) throws IOException {
byte[] buffer = new byte[length];
int total = 0;
while (total < length) {
int count = read(buffer, total, length - total);
if (count == -1) {
break;
}
total += count;
}
if (total != length) {
throw new IOException(String.format("Read wrong number of bytes. Got: %s, Expected: %s.", total, length));
}
return buffer;
}
}
}
================================================
FILE: src/com/codebutler/android_websockets/WebSocketClient.java
================================================
package com.codebutler.android_websockets;
import android.os.Handler;
import android.os.HandlerThread;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import org.apache.http.*;
import org.apache.http.client.HttpResponseException;
import org.apache.http.message.BasicLineParser;
import org.apache.http.message.BasicNameValuePair;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
public class WebSocketClient {
private static final String TAG = "WebSocketClient";
private URI mURI;
private Listener mListener;
private Socket mSocket;
private Thread mThread;
private HandlerThread mHandlerThread;
private Handler mHandler;
private List mExtraHeaders;
private HybiParser mParser;
private boolean mConnected;
private final Object mSendLock = new Object();
private static TrustManager[] sTrustManagers;
public static void setTrustManagers(TrustManager[] tm) {
sTrustManagers = tm;
}
public WebSocketClient(URI uri, Listener listener, List extraHeaders) {
mURI = uri;
mListener = listener;
mExtraHeaders = extraHeaders;
mConnected = false;
mParser = new HybiParser(this);
mHandlerThread = new HandlerThread("websocket-thread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
public Listener getListener() {
return mListener;
}
public void connect() {
if (mThread != null && mThread.isAlive()) {
return;
}
mThread = new Thread(new Runnable() {
@Override
public void run() {
try {
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())) {
path += "?" + mURI.getQuery();
}
String originScheme = mURI.getScheme().equals("wss") ? "https" : "http";
URI origin = new URI(originScheme, "//" + mURI.getHost(), null);
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: " + secretKey + "\r\n");
out.print("Sec-WebSocket-Version: 13\r\n");
if (mExtraHeaders != null) {
for (NameValuePair pair : mExtraHeaders) {
out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue()));
}
}
out.print("\r\n");
out.flush();
HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream());
// Read HTTP response status line.
StatusLine statusLine = parseStatusLine(readLine(stream));
if (statusLine == null) {
throw new HttpException("Received no reply from server.");
} else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) {
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
// Read HTTP response headers.
String line;
while (!TextUtils.isEmpty(line = readLine(stream))) {
Header header = parseHeader(line);
if (header.getName().equals("Sec-WebSocket-Accept")) {
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);
}
}
});
mThread.start();
}
public void disconnect() {
if (mSocket != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mSocket != null) {
try {
mSocket.close();
} catch (IOException ex) {
Log.d(TAG, "Error while disconnecting", ex);
mListener.onError(ex);
}
mSocket = null;
}
mConnected = false;
}
});
}
}
public void send(String data) {
sendFrame(mParser.frame(data));
}
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;
}
return BasicLineParser.parseStatusLine(line, new BasicLineParser());
}
private Header parseHeader(String line) {
return BasicLineParser.parseHeader(line, new BasicLineParser());
}
// Can't use BufferedReader because it buffers past the HTTP data.
private String readLine(HybiParser.HappyDataInputStream reader) throws IOException {
int readChar = reader.read();
if (readChar == -1) {
return null;
}
StringBuilder string = new StringBuilder("");
while (readChar != '\n') {
if (readChar != '\r') {
string.append((char) readChar);
}
readChar = reader.read();
if (readChar == -1) {
return null;
}
}
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++) {
nonce[i] = (byte) (Math.random() * 256);
}
return Base64.encodeToString(nonce, Base64.DEFAULT).trim();
}
void sendFrame(final byte[] frame) {
mHandler.post(new Runnable() {
@Override
public void run() {
try {
synchronized (mSendLock) {
OutputStream outputStream = mSocket.getOutputStream();
outputStream.write(frame);
outputStream.flush();
}
} catch (IOException e) {
mListener.onError(e);
}
}
});
}
public interface Listener {
public void onConnect();
public void onMessage(String message);
public void onMessage(byte[] data);
public void onDisconnect(int code, String reason);
public void onError(Exception error);
}
private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, sTrustManagers, null);
return context.getSocketFactory();
}
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/Acknowledge.java
================================================
package com.koushikdutta.async.http.socketio;
import org.json.JSONArray;
public interface Acknowledge {
void acknowledge(JSONArray arguments);
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/ConnectCallback.java
================================================
package com.koushikdutta.async.http.socketio;
public interface ConnectCallback {
public void onConnectCompleted(Exception ex, SocketIOClient client);
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/DisconnectCallback.java
================================================
package com.koushikdutta.async.http.socketio;
/**
* Created by koush on 7/2/13.
*/
public interface DisconnectCallback {
void onDisconnect(Exception e);
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/ErrorCallback.java
================================================
package com.koushikdutta.async.http.socketio;
/**
* Created by koush on 7/2/13.
*/
public interface ErrorCallback {
void onError(String error);
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/EventCallback.java
================================================
package com.koushikdutta.async.http.socketio;
import org.json.JSONArray;
public interface EventCallback {
public void onEvent(String event, JSONArray argument, Acknowledge acknowledge);
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/EventEmitter.java
================================================
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);
}
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/JSONCallback.java
================================================
package com.koushikdutta.async.http.socketio;
import org.json.JSONObject;
public interface JSONCallback {
public void onJSON(JSONObject json, Acknowledge acknowledge);
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/ReconnectCallback.java
================================================
package com.koushikdutta.async.http.socketio;
public interface ReconnectCallback {
public void onReconnect();
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/SocketIOClient.java
================================================
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;
}
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/SocketIOConnection.java
================================================
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));
}
};
}
}
================================================
FILE: src/com/koushikdutta/async/http/socketio/StringCallback.java
================================================
package com.koushikdutta.async.http.socketio;
public interface StringCallback {
public void onString(String string, Acknowledge acknowledge);
}
================================================
FILE: src/com/koushikdutta/async/util/HashList.java
================================================
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);
}
}
================================================
FILE: src/com/koushikdutta/http/AsyncHttpClient.java
================================================
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));
}
}