├── .gitignore ├── AndroidManifest.xml ├── README.md ├── android-websockets.iml ├── ant.properties ├── build.xml ├── proguard-project.txt ├── project.properties └── 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 /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | .classpath 4 | .project 5 | local.properties 6 | /.settings 7 | project.properties 8 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS LIBRARY IS DEPRECATED IN FAVOR OF: 2 | 3 | [AndroidAsync](https://github.com/koush/AndroidAsync) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | # WebSocket and Socket.IO client for Android 13 | 14 | ## Credits 15 | 16 | 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! 17 | 18 | The hybi parser was ported from JavaScript to Java by [Eric Butler](https://twitter.com/codebutler) . 19 | 20 | The WebSocket client was written by [Eric Butler](https://twitter.com/codebutler) . 21 | 22 | The Socket.IO client was written by [Koushik Dutta](https://twitter.com/koush). 23 | 24 | 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) 25 | 26 | ## WebSocket Usage 27 | 28 | ```java 29 | List extraHeaders = Arrays.asList( 30 | new BasicNameValuePair("Cookie", "session=abcd") 31 | ); 32 | 33 | WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Listener() { 34 | @Override 35 | public void onConnect() { 36 | Log.d(TAG, "Connected!"); 37 | } 38 | 39 | @Override 40 | public void onMessage(String message) { 41 | Log.d(TAG, String.format("Got string message! %s", message)); 42 | } 43 | 44 | @Override 45 | public void onMessage(byte[] data) { 46 | Log.d(TAG, String.format("Got binary message! %s", toHexString(data))); 47 | } 48 | 49 | @Override 50 | public void onDisconnect(int code, String reason) { 51 | Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason)); 52 | } 53 | 54 | @Override 55 | public void onError(Exception error) { 56 | Log.e(TAG, "Error!", error); 57 | } 58 | 59 | }, extraHeaders); 60 | 61 | client.connect(); 62 | 63 | // Later… 64 | client.send("hello!"); 65 | client.send(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }); 66 | client.disconnect(); 67 | ``` 68 | 69 | ## Socket.IO Usage 70 | 71 | ```java 72 | SocketIOClient.connect("http://localhost:80", new ConnectCallback() { 73 | 74 | @Override 75 | public void onConnectCompleted(Exception ex, SocketIOClient client) { 76 | 77 | if (ex != null) { 78 | return; 79 | } 80 | 81 | //Save the returned SocketIOClient instance into a variable so you can disconnect it later 82 | client.setDisconnectCallback(MainActivity.this); 83 | client.setErrorCallback(MainActivity.this); 84 | client.setJSONCallback(MainActivity.this); 85 | client.setStringCallback(MainActivity.this); 86 | 87 | //You need to explicitly specify which events you are interested in receiving 88 | client.addListener("news", MainActivity.this); 89 | 90 | client.of("/chat", new ConnectCallback() { 91 | 92 | @Override 93 | public void onConnectCompleted(Exception ex, SocketIOClient client) { 94 | 95 | if (ex != null) { 96 | ex.printStackTrace(); 97 | return; 98 | } 99 | 100 | //This client instance will be using the same websocket as the original client, 101 | //but will point to the indicated endpoint 102 | client.setDisconnectCallback(MainActivity.this); 103 | client.setErrorCallback(MainActivity.this); 104 | client.setJSONCallback(MainActivity.this); 105 | client.setStringCallback(MainActivity.this); 106 | client.addListener("a message", MainActivity.this); 107 | 108 | } 109 | }); 110 | 111 | } 112 | }, new Handler()); 113 | 114 | 115 | @Override 116 | public void onEvent(String event, JSONArray argument, Acknowledge acknowledge) { 117 | try { 118 | Log.d("MainActivity", "Event:" + event + "Arguments:" 119 | + argument.toString(2)); 120 | } catch (JSONException e) { 121 | e.printStackTrace(); 122 | } 123 | 124 | } 125 | 126 | @Override 127 | public void onString(String string, Acknowledge acknowledge) { 128 | Log.d("MainActivity", string); 129 | 130 | } 131 | 132 | @Override 133 | public void onJSON(JSONObject json, Acknowledge acknowledge) { 134 | try { 135 | Log.d("MainActivity", "json:" + json.toString(2)); 136 | } catch (JSONException e) { 137 | e.printStackTrace(); 138 | } 139 | 140 | } 141 | 142 | @Override 143 | public void onError(String error) { 144 | Log.d("MainActivity", error); 145 | 146 | } 147 | 148 | @Override 149 | public void onDisconnect(Exception e) { 150 | Log.d(mComponentTag, "Disconnected:" + e.getMessage()); 151 | 152 | } 153 | 154 | ``` 155 | 156 | 157 | 158 | ## TODO 159 | 160 | * Run [autobahn tests](http://autobahn.ws/testsuite) 161 | * Investigate using [naga](http://code.google.com/p/naga/) instead of threads. 162 | 163 | ## License 164 | 165 | (The MIT License) 166 | 167 | Copyright (c) 2009-2012 James Coglan 168 | Copyright (c) 2012 Eric Butler 169 | Copyright (c) 2012 Koushik Dutta 170 | 171 | Permission is hereby granted, free of charge, to any person obtaining a copy of 172 | this software and associated documentation files (the 'Software'), to deal in 173 | the Software without restriction, including without limitation the rights to use, 174 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 175 | Software, and to permit persons to whom the Software is furnished to do so, 176 | subject to the following conditions: 177 | 178 | The above copyright notice and this permission notice shall be included in all 179 | copies or substantial portions of the Software. 180 | 181 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 182 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 183 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 184 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 185 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 186 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 187 | 188 | -------------------------------------------------------------------------------- /android-websockets.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ant.properties: -------------------------------------------------------------------------------- 1 | # This file is used to override default values used by the Ant build system. 2 | # 3 | # This file must be checked into Version Control Systems, as it is 4 | # integral to the build system of your project. 5 | 6 | # This file is only used by the Ant script. 7 | 8 | # You can use this to override default values such as 9 | # 'source.dir' for the location of your java source folder and 10 | # 'out.dir' for the location of your output folder. 11 | 12 | # You can also use it define how the release builds are signed by declaring 13 | # the following properties: 14 | # 'key.store' for the location of your keystore and 15 | # 'key.alias' for the name of the key to use. 16 | # The password will be asked during the build when you use the 'release' target. 17 | 18 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | android.library=true 14 | # Project target. 15 | target=android-8 16 | -------------------------------------------------------------------------------- /src/com/codebutler/android_websockets/HybiParser.java: -------------------------------------------------------------------------------- 1 | // 2 | // HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser 3 | // 4 | // Based on code from the faye project. 5 | // https://github.com/faye/faye-websocket-node 6 | // Copyright (c) 2009-2012 James Coglan 7 | // 8 | // Ported from Javascript to Java by Eric Butler 9 | // 10 | // (The MIT License) 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining 13 | // a copy of this software and associated documentation files (the 14 | // "Software"), to deal in the Software without restriction, including 15 | // without limitation the rights to use, copy, modify, merge, publish, 16 | // distribute, sublicense, and/or sell copies of the Software, and to 17 | // permit persons to whom the Software is furnished to do so, subject to 18 | // the following conditions: 19 | // 20 | // The above copyright notice and this permission notice shall be 21 | // included in all copies or substantial portions of the Software. 22 | // 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | package com.codebutler.android_websockets; 32 | 33 | import android.util.Log; 34 | 35 | import java.io.*; 36 | import java.util.Arrays; 37 | import java.util.List; 38 | 39 | public class HybiParser { 40 | private static final String TAG = "HybiParser"; 41 | 42 | private WebSocketClient mClient; 43 | 44 | private boolean mMasking = true; 45 | 46 | private int mStage; 47 | 48 | private boolean mFinal; 49 | private boolean mMasked; 50 | private int mOpcode; 51 | private int mLengthSize; 52 | private int mLength; 53 | private int mMode; 54 | 55 | private byte[] mMask = new byte[0]; 56 | private byte[] mPayload = new byte[0]; 57 | 58 | private boolean mClosed = false; 59 | 60 | private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); 61 | 62 | private static final int BYTE = 255; 63 | private static final int FIN = 128; 64 | private static final int MASK = 128; 65 | private static final int RSV1 = 64; 66 | private static final int RSV2 = 32; 67 | private static final int RSV3 = 16; 68 | private static final int OPCODE = 15; 69 | private static final int LENGTH = 127; 70 | 71 | private static final int MODE_TEXT = 1; 72 | private static final int MODE_BINARY = 2; 73 | 74 | private static final int OP_CONTINUATION = 0; 75 | private static final int OP_TEXT = 1; 76 | private static final int OP_BINARY = 2; 77 | private static final int OP_CLOSE = 8; 78 | private static final int OP_PING = 9; 79 | private static final int OP_PONG = 10; 80 | 81 | private static final List OPCODES = Arrays.asList( 82 | OP_CONTINUATION, 83 | OP_TEXT, 84 | OP_BINARY, 85 | OP_CLOSE, 86 | OP_PING, 87 | OP_PONG 88 | ); 89 | 90 | private static final List FRAGMENTED_OPCODES = Arrays.asList( 91 | OP_CONTINUATION, OP_TEXT, OP_BINARY 92 | ); 93 | 94 | public HybiParser(WebSocketClient client) { 95 | mClient = client; 96 | } 97 | 98 | private static byte[] mask(byte[] payload, byte[] mask, int offset) { 99 | if (mask.length == 0) return payload; 100 | 101 | for (int i = 0; i < payload.length - offset; i++) { 102 | payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]); 103 | } 104 | return payload; 105 | } 106 | 107 | public void start(HappyDataInputStream stream) throws IOException { 108 | while (true) { 109 | if (stream.available() == -1) break; 110 | switch (mStage) { 111 | case 0: 112 | parseOpcode(stream.readByte()); 113 | break; 114 | case 1: 115 | parseLength(stream.readByte()); 116 | break; 117 | case 2: 118 | parseExtendedLength(stream.readBytes(mLengthSize)); 119 | break; 120 | case 3: 121 | mMask = stream.readBytes(4); 122 | mStage = 4; 123 | break; 124 | case 4: 125 | mPayload = stream.readBytes(mLength); 126 | emitFrame(); 127 | mStage = 0; 128 | break; 129 | } 130 | } 131 | mClient.getListener().onDisconnect(0, "EOF"); 132 | } 133 | 134 | private void parseOpcode(byte data) throws ProtocolError { 135 | boolean rsv1 = (data & RSV1) == RSV1; 136 | boolean rsv2 = (data & RSV2) == RSV2; 137 | boolean rsv3 = (data & RSV3) == RSV3; 138 | 139 | if (rsv1 || rsv2 || rsv3) { 140 | throw new ProtocolError("RSV not zero"); 141 | } 142 | 143 | mFinal = (data & FIN) == FIN; 144 | mOpcode = (data & OPCODE); 145 | mMask = new byte[0]; 146 | mPayload = new byte[0]; 147 | 148 | if (!OPCODES.contains(mOpcode)) { 149 | throw new ProtocolError("Bad opcode"); 150 | } 151 | 152 | if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) { 153 | throw new ProtocolError("Expected non-final packet"); 154 | } 155 | 156 | mStage = 1; 157 | } 158 | 159 | private void parseLength(byte data) { 160 | mMasked = (data & MASK) == MASK; 161 | mLength = (data & LENGTH); 162 | 163 | if (mLength >= 0 && mLength <= 125) { 164 | mStage = mMasked ? 3 : 4; 165 | } else { 166 | mLengthSize = (mLength == 126) ? 2 : 8; 167 | mStage = 2; 168 | } 169 | } 170 | 171 | private void parseExtendedLength(byte[] buffer) throws ProtocolError { 172 | mLength = getInteger(buffer); 173 | mStage = mMasked ? 3 : 4; 174 | } 175 | 176 | public byte[] frame(String data) { 177 | return frame(data, OP_TEXT, -1); 178 | } 179 | 180 | public byte[] frame(byte[] data) { 181 | return frame(data, OP_BINARY, -1); 182 | } 183 | 184 | private byte[] frame(byte[] data, int opcode, int errorCode) { 185 | return frame((Object)data, opcode, errorCode); 186 | } 187 | 188 | private byte[] frame(String data, int opcode, int errorCode) { 189 | return frame((Object)data, opcode, errorCode); 190 | } 191 | 192 | private byte[] frame(Object data, int opcode, int errorCode) { 193 | if (mClosed) return null; 194 | 195 | Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode); 196 | 197 | byte[] buffer = (data instanceof String) ? decode((String) data) : (byte[]) data; 198 | int insert = (errorCode > 0) ? 2 : 0; 199 | int length = buffer.length + insert; 200 | int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10); 201 | int offset = header + (mMasking ? 4 : 0); 202 | int masked = mMasking ? MASK : 0; 203 | byte[] frame = new byte[length + offset]; 204 | 205 | frame[0] = (byte) ((byte)FIN | (byte)opcode); 206 | 207 | if (length <= 125) { 208 | frame[1] = (byte) (masked | length); 209 | } else if (length <= 65535) { 210 | frame[1] = (byte) (masked | 126); 211 | frame[2] = (byte) Math.floor(length / 256); 212 | frame[3] = (byte) (length & BYTE); 213 | } else { 214 | frame[1] = (byte) (masked | 127); 215 | frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE); 216 | frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE); 217 | frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE); 218 | frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE); 219 | frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE); 220 | frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE); 221 | frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE); 222 | frame[9] = (byte) (length & BYTE); 223 | } 224 | 225 | if (errorCode > 0) { 226 | frame[offset] = (byte) (((int) Math.floor(errorCode / 256)) & BYTE); 227 | frame[offset+1] = (byte) (errorCode & BYTE); 228 | } 229 | System.arraycopy(buffer, 0, frame, offset + insert, buffer.length); 230 | 231 | if (mMasking) { 232 | byte[] mask = { 233 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), 234 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256) 235 | }; 236 | System.arraycopy(mask, 0, frame, header, mask.length); 237 | mask(frame, mask, offset); 238 | } 239 | 240 | return frame; 241 | } 242 | 243 | public void ping(String message) { 244 | mClient.send(frame(message, OP_PING, -1)); 245 | } 246 | 247 | public void close(int code, String reason) { 248 | if (mClosed) return; 249 | mClient.send(frame(reason, OP_CLOSE, code)); 250 | mClosed = true; 251 | } 252 | 253 | private void emitFrame() throws IOException { 254 | byte[] payload = mask(mPayload, mMask, 0); 255 | int opcode = mOpcode; 256 | 257 | if (opcode == OP_CONTINUATION) { 258 | if (mMode == 0) { 259 | throw new ProtocolError("Mode was not set."); 260 | } 261 | mBuffer.write(payload); 262 | if (mFinal) { 263 | byte[] message = mBuffer.toByteArray(); 264 | if (mMode == MODE_TEXT) { 265 | mClient.getListener().onMessage(encode(message)); 266 | } else { 267 | mClient.getListener().onMessage(message); 268 | } 269 | reset(); 270 | } 271 | 272 | } else if (opcode == OP_TEXT) { 273 | if (mFinal) { 274 | String messageText = encode(payload); 275 | mClient.getListener().onMessage(messageText); 276 | } else { 277 | mMode = MODE_TEXT; 278 | mBuffer.write(payload); 279 | } 280 | 281 | } else if (opcode == OP_BINARY) { 282 | if (mFinal) { 283 | mClient.getListener().onMessage(payload); 284 | } else { 285 | mMode = MODE_BINARY; 286 | mBuffer.write(payload); 287 | } 288 | 289 | } else if (opcode == OP_CLOSE) { 290 | int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0; 291 | String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null; 292 | Log.d(TAG, "Got close op! " + code + " " + reason); 293 | mClient.getListener().onDisconnect(code, reason); 294 | 295 | } else if (opcode == OP_PING) { 296 | if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); } 297 | Log.d(TAG, "Sending pong!!"); 298 | mClient.sendFrame(frame(payload, OP_PONG, -1)); 299 | 300 | } else if (opcode == OP_PONG) { 301 | String message = encode(payload); 302 | // FIXME: Fire callback... 303 | Log.d(TAG, "Got pong! " + message); 304 | } 305 | } 306 | 307 | private void reset() { 308 | mMode = 0; 309 | mBuffer.reset(); 310 | } 311 | 312 | private String encode(byte[] buffer) { 313 | try { 314 | return new String(buffer, "UTF-8"); 315 | } catch (UnsupportedEncodingException e) { 316 | throw new RuntimeException(e); 317 | } 318 | } 319 | 320 | private byte[] decode(String string) { 321 | try { 322 | return (string).getBytes("UTF-8"); 323 | } catch (UnsupportedEncodingException e) { 324 | throw new RuntimeException(e); 325 | } 326 | } 327 | 328 | private int getInteger(byte[] bytes) throws ProtocolError { 329 | long i = byteArrayToLong(bytes, 0, bytes.length); 330 | if (i < 0 || i > Integer.MAX_VALUE) { 331 | throw new ProtocolError("Bad integer: " + i); 332 | } 333 | return (int) i; 334 | } 335 | 336 | /** 337 | * Copied from AOSP Arrays.java. 338 | */ 339 | /** 340 | * Copies elements from {@code original} into a new array, from indexes start (inclusive) to 341 | * end (exclusive). The original order of elements is preserved. 342 | * If {@code end} is greater than {@code original.length}, the result is padded 343 | * with the value {@code (byte) 0}. 344 | * 345 | * @param original the original array 346 | * @param start the start index, inclusive 347 | * @param end the end index, exclusive 348 | * @return the new array 349 | * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length} 350 | * @throws IllegalArgumentException if {@code start > end} 351 | * @throws NullPointerException if {@code original == null} 352 | * @since 1.6 353 | */ 354 | private static byte[] copyOfRange(byte[] original, int start, int end) { 355 | if (start > end) { 356 | throw new IllegalArgumentException(); 357 | } 358 | int originalLength = original.length; 359 | if (start < 0 || start > originalLength) { 360 | throw new ArrayIndexOutOfBoundsException(); 361 | } 362 | int resultLength = end - start; 363 | int copyLength = Math.min(resultLength, originalLength - start); 364 | byte[] result = new byte[resultLength]; 365 | System.arraycopy(original, start, result, 0, copyLength); 366 | return result; 367 | } 368 | 369 | private byte[] slice(byte[] array, int start) { 370 | return copyOfRange(array, start, array.length); 371 | } 372 | 373 | public static class ProtocolError extends IOException { 374 | public ProtocolError(String detailMessage) { 375 | super(detailMessage); 376 | } 377 | } 378 | 379 | private static long byteArrayToLong(byte[] b, int offset, int length) { 380 | if (b.length < length) 381 | throw new IllegalArgumentException("length must be less than or equal to b.length"); 382 | 383 | long value = 0; 384 | for (int i = 0; i < length; i++) { 385 | int shift = (length - 1 - i) * 8; 386 | value += (b[i + offset] & 0x000000FF) << shift; 387 | } 388 | return value; 389 | } 390 | 391 | public static class HappyDataInputStream extends DataInputStream { 392 | public HappyDataInputStream(InputStream in) { 393 | super(in); 394 | } 395 | 396 | public byte[] readBytes(int length) throws IOException { 397 | byte[] buffer = new byte[length]; 398 | 399 | int total = 0; 400 | 401 | while (total < length) { 402 | int count = read(buffer, total, length - total); 403 | if (count == -1) { 404 | break; 405 | } 406 | total += count; 407 | } 408 | 409 | if (total != length) { 410 | throw new IOException(String.format("Read wrong number of bytes. Got: %s, Expected: %s.", total, length)); 411 | } 412 | 413 | return buffer; 414 | } 415 | } 416 | } -------------------------------------------------------------------------------- /src/com/codebutler/android_websockets/WebSocketClient.java: -------------------------------------------------------------------------------- 1 | package com.codebutler.android_websockets; 2 | 3 | import android.os.Handler; 4 | import android.os.HandlerThread; 5 | import android.text.TextUtils; 6 | import android.util.Base64; 7 | import android.util.Log; 8 | import org.apache.http.*; 9 | import org.apache.http.client.HttpResponseException; 10 | import org.apache.http.message.BasicLineParser; 11 | import org.apache.http.message.BasicNameValuePair; 12 | 13 | import javax.net.SocketFactory; 14 | import javax.net.ssl.SSLContext; 15 | import javax.net.ssl.SSLException; 16 | import javax.net.ssl.SSLSocketFactory; 17 | import javax.net.ssl.TrustManager; 18 | import java.io.EOFException; 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.io.PrintWriter; 22 | import java.net.Socket; 23 | import java.net.URI; 24 | import java.security.KeyManagementException; 25 | import java.security.MessageDigest; 26 | import java.security.NoSuchAlgorithmException; 27 | import java.util.List; 28 | 29 | public class WebSocketClient { 30 | private static final String TAG = "WebSocketClient"; 31 | 32 | private URI mURI; 33 | private Listener mListener; 34 | private Socket mSocket; 35 | private Thread mThread; 36 | private HandlerThread mHandlerThread; 37 | private Handler mHandler; 38 | private List mExtraHeaders; 39 | private HybiParser mParser; 40 | private boolean mConnected; 41 | 42 | private final Object mSendLock = new Object(); 43 | 44 | private static TrustManager[] sTrustManagers; 45 | 46 | public static void setTrustManagers(TrustManager[] tm) { 47 | sTrustManagers = tm; 48 | } 49 | 50 | public WebSocketClient(URI uri, Listener listener, List extraHeaders) { 51 | mURI = uri; 52 | mListener = listener; 53 | mExtraHeaders = extraHeaders; 54 | mConnected = false; 55 | mParser = new HybiParser(this); 56 | 57 | mHandlerThread = new HandlerThread("websocket-thread"); 58 | mHandlerThread.start(); 59 | mHandler = new Handler(mHandlerThread.getLooper()); 60 | } 61 | 62 | public Listener getListener() { 63 | return mListener; 64 | } 65 | 66 | public void connect() { 67 | if (mThread != null && mThread.isAlive()) { 68 | return; 69 | } 70 | 71 | mThread = new Thread(new Runnable() { 72 | @Override 73 | public void run() { 74 | try { 75 | int port = (mURI.getPort() != -1) ? mURI.getPort() : ((mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? 443 : 80); 76 | 77 | String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath(); 78 | if (!TextUtils.isEmpty(mURI.getQuery())) { 79 | path += "?" + mURI.getQuery(); 80 | } 81 | 82 | String originScheme = mURI.getScheme().equals("wss") ? "https" : "http"; 83 | URI origin = new URI(originScheme, "//" + mURI.getHost(), null); 84 | 85 | SocketFactory factory = (mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? getSSLSocketFactory() : SocketFactory.getDefault(); 86 | mSocket = factory.createSocket(mURI.getHost(), port); 87 | 88 | PrintWriter out = new PrintWriter(mSocket.getOutputStream()); 89 | String secretKey = createSecret(); 90 | out.print("GET " + path + " HTTP/1.1\r\n"); 91 | out.print("Upgrade: websocket\r\n"); 92 | out.print("Connection: Upgrade\r\n"); 93 | out.print("Host: " + mURI.getHost() + "\r\n"); 94 | out.print("Origin: " + origin.toString() + "\r\n"); 95 | out.print("Sec-WebSocket-Key: " + secretKey + "\r\n"); 96 | out.print("Sec-WebSocket-Version: 13\r\n"); 97 | if (mExtraHeaders != null) { 98 | for (NameValuePair pair : mExtraHeaders) { 99 | out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue())); 100 | } 101 | } 102 | out.print("\r\n"); 103 | out.flush(); 104 | 105 | HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream()); 106 | 107 | // Read HTTP response status line. 108 | StatusLine statusLine = parseStatusLine(readLine(stream)); 109 | if (statusLine == null) { 110 | throw new HttpException("Received no reply from server."); 111 | } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) { 112 | throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); 113 | } 114 | 115 | // Read HTTP response headers. 116 | String line; 117 | while (!TextUtils.isEmpty(line = readLine(stream))) { 118 | Header header = parseHeader(line); 119 | if (header.getName().equals("Sec-WebSocket-Accept")) { 120 | String expected = expectedKey(secretKey); 121 | if (expected == null) { 122 | throw new Exception("SHA-1 algorithm not found"); 123 | } else if (!expected.equals(header.getValue())) { 124 | throw new Exception("Invalid Sec-WebSocket-Accept, expected: " + expected + ", got: " + header.getValue()); 125 | } 126 | } 127 | } 128 | 129 | mListener.onConnect(); 130 | 131 | mConnected = true; 132 | 133 | // Now decode websocket frames. 134 | mParser.start(stream); 135 | 136 | } catch (EOFException ex) { 137 | Log.d(TAG, "WebSocket EOF!", ex); 138 | mListener.onDisconnect(0, "EOF"); 139 | mConnected = false; 140 | 141 | } catch (SSLException ex) { 142 | // Connection reset by peer 143 | Log.d(TAG, "Websocket SSL error!", ex); 144 | mListener.onDisconnect(0, "SSL"); 145 | mConnected = false; 146 | 147 | } catch (Exception ex) { 148 | mListener.onError(ex); 149 | } 150 | } 151 | }); 152 | mThread.start(); 153 | } 154 | 155 | public void disconnect() { 156 | if (mSocket != null) { 157 | mHandler.post(new Runnable() { 158 | @Override 159 | public void run() { 160 | if (mSocket != null) { 161 | try { 162 | mSocket.close(); 163 | } catch (IOException ex) { 164 | Log.d(TAG, "Error while disconnecting", ex); 165 | mListener.onError(ex); 166 | } 167 | mSocket = null; 168 | } 169 | mConnected = false; 170 | } 171 | }); 172 | } 173 | } 174 | 175 | public void send(String data) { 176 | sendFrame(mParser.frame(data)); 177 | } 178 | 179 | public void send(byte[] data) { 180 | sendFrame(mParser.frame(data)); 181 | } 182 | 183 | public boolean isConnected() { 184 | return mConnected; 185 | } 186 | 187 | private StatusLine parseStatusLine(String line) { 188 | if (TextUtils.isEmpty(line)) { 189 | return null; 190 | } 191 | return BasicLineParser.parseStatusLine(line, new BasicLineParser()); 192 | } 193 | 194 | private Header parseHeader(String line) { 195 | return BasicLineParser.parseHeader(line, new BasicLineParser()); 196 | } 197 | 198 | // Can't use BufferedReader because it buffers past the HTTP data. 199 | private String readLine(HybiParser.HappyDataInputStream reader) throws IOException { 200 | int readChar = reader.read(); 201 | if (readChar == -1) { 202 | return null; 203 | } 204 | StringBuilder string = new StringBuilder(""); 205 | while (readChar != '\n') { 206 | if (readChar != '\r') { 207 | string.append((char) readChar); 208 | } 209 | 210 | readChar = reader.read(); 211 | if (readChar == -1) { 212 | return null; 213 | } 214 | } 215 | return string.toString(); 216 | } 217 | 218 | private String expectedKey(String secret) { 219 | //concatenate, SHA1-hash, base64-encode 220 | try { 221 | final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 222 | final String secretGUID = secret + GUID; 223 | MessageDigest md = MessageDigest.getInstance("SHA-1"); 224 | byte[] digest = md.digest(secretGUID.getBytes()); 225 | return Base64.encodeToString(digest, Base64.DEFAULT).trim(); 226 | } catch (NoSuchAlgorithmException e) { 227 | return null; 228 | } 229 | } 230 | 231 | private String createSecret() { 232 | byte[] nonce = new byte[16]; 233 | for (int i = 0; i < 16; i++) { 234 | nonce[i] = (byte) (Math.random() * 256); 235 | } 236 | return Base64.encodeToString(nonce, Base64.DEFAULT).trim(); 237 | } 238 | 239 | void sendFrame(final byte[] frame) { 240 | mHandler.post(new Runnable() { 241 | @Override 242 | public void run() { 243 | try { 244 | synchronized (mSendLock) { 245 | OutputStream outputStream = mSocket.getOutputStream(); 246 | outputStream.write(frame); 247 | outputStream.flush(); 248 | } 249 | } catch (IOException e) { 250 | mListener.onError(e); 251 | } 252 | } 253 | }); 254 | } 255 | 256 | public interface Listener { 257 | public void onConnect(); 258 | public void onMessage(String message); 259 | public void onMessage(byte[] data); 260 | public void onDisconnect(int code, String reason); 261 | public void onError(Exception error); 262 | } 263 | 264 | private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { 265 | SSLContext context = SSLContext.getInstance("TLS"); 266 | context.init(null, sTrustManagers, null); 267 | return context.getSocketFactory(); 268 | } 269 | } -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/Acknowledge.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | import org.json.JSONArray; 4 | 5 | public interface Acknowledge { 6 | void acknowledge(JSONArray arguments); 7 | } 8 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/ConnectCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | public interface ConnectCallback { 4 | public void onConnectCompleted(Exception ex, SocketIOClient client); 5 | } -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/DisconnectCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | /** 4 | * Created by koush on 7/2/13. 5 | */ 6 | public interface DisconnectCallback { 7 | void onDisconnect(Exception e); 8 | } 9 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/ErrorCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | /** 4 | * Created by koush on 7/2/13. 5 | */ 6 | public interface ErrorCallback { 7 | void onError(String error); 8 | } 9 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/EventCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | import org.json.JSONArray; 4 | 5 | public interface EventCallback { 6 | public void onEvent(String event, JSONArray argument, Acknowledge acknowledge); 7 | } -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/EventEmitter.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | import com.koushikdutta.async.util.HashList; 4 | 5 | import org.json.JSONArray; 6 | 7 | import java.util.Iterator; 8 | import java.util.List; 9 | 10 | /** 11 | * Created by koush on 7/1/13. 12 | */ 13 | public class EventEmitter { 14 | interface OnceCallback extends EventCallback { 15 | } 16 | 17 | HashList callbacks = new HashList(); 18 | 19 | void onEvent(String event, JSONArray arguments, Acknowledge acknowledge) { 20 | List list = callbacks.get(event); 21 | if (list == null) 22 | return; 23 | Iterator iter = list.iterator(); 24 | while (iter.hasNext()) { 25 | EventCallback cb = iter.next(); 26 | cb.onEvent(event, arguments, acknowledge); 27 | if (cb instanceof OnceCallback) 28 | iter.remove(); 29 | } 30 | } 31 | 32 | public void addListener(String event, EventCallback callback) { 33 | on(event, callback); 34 | } 35 | 36 | public void once(final String event, final EventCallback callback) { 37 | on(event, new OnceCallback() { 38 | @Override 39 | public void onEvent(String event, JSONArray arguments, Acknowledge acknowledge) { 40 | callback.onEvent(event, arguments, acknowledge); 41 | } 42 | }); 43 | } 44 | 45 | public void on(String event, EventCallback callback) { 46 | callbacks.add(event, callback); 47 | } 48 | 49 | public void removeListener(String event, EventCallback callback) { 50 | List list = callbacks.get(event); 51 | if (list == null) 52 | return; 53 | list.remove(callback); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/JSONCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | import org.json.JSONObject; 4 | 5 | public interface JSONCallback { 6 | public void onJSON(JSONObject json, Acknowledge acknowledge); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/ReconnectCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | public interface ReconnectCallback { 4 | public void onReconnect(); 5 | } -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/SocketIOClient.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | 6 | import android.os.Handler; 7 | import android.text.TextUtils; 8 | 9 | import com.codebutler.android_websockets.WebSocketClient; 10 | import com.koushikdutta.http.AsyncHttpClient; 11 | import com.koushikdutta.http.AsyncHttpClient.SocketIORequest; 12 | 13 | public class SocketIOClient extends EventEmitter { 14 | 15 | boolean connected; 16 | boolean disconnected; 17 | Handler handler; 18 | 19 | private void emitRaw(int type, String message, Acknowledge acknowledge) { 20 | connection.emitRaw(type, this, message, acknowledge); 21 | } 22 | 23 | public void emit(String name, JSONArray args) { 24 | emit(name, args, null); 25 | } 26 | 27 | public void emit(final String message) { 28 | emit(message, (Acknowledge) null); 29 | } 30 | 31 | public void emit(final JSONObject jsonMessage) { 32 | emit(jsonMessage, null); 33 | } 34 | 35 | public void emit(String name, JSONArray args, Acknowledge acknowledge) { 36 | final JSONObject event = new JSONObject(); 37 | try { 38 | event.put("name", name); 39 | event.put("args", args); 40 | emitRaw(5, event.toString(), acknowledge); 41 | } catch (Exception e) { 42 | } 43 | } 44 | 45 | public void emit(final String message, Acknowledge acknowledge) { 46 | emitRaw(3, message, acknowledge); 47 | } 48 | 49 | public void emit(final JSONObject jsonMessage, Acknowledge acknowledge) { 50 | emitRaw(4, jsonMessage.toString(), acknowledge); 51 | } 52 | 53 | public static void connect(String uri, final ConnectCallback callback, final Handler handler) { 54 | connect(new SocketIORequest(uri), callback, handler); 55 | } 56 | 57 | ConnectCallback connectCallback; 58 | 59 | public static void connect(final SocketIORequest request, final ConnectCallback callback, final Handler handler) { 60 | 61 | final SocketIOConnection connection = new SocketIOConnection(handler, new AsyncHttpClient(), request); 62 | 63 | final ConnectCallback wrappedCallback = new ConnectCallback() { 64 | @Override 65 | public void onConnectCompleted(Exception ex, SocketIOClient client) { 66 | if (ex != null || TextUtils.isEmpty(request.getEndpoint())) { 67 | 68 | client.handler = handler; 69 | if (callback != null) { 70 | callback.onConnectCompleted(ex, client); 71 | } 72 | 73 | return; 74 | } 75 | 76 | // remove the root client since that's not actually being used. 77 | connection.clients.remove(client); 78 | 79 | // connect to the endpoint we want 80 | client.of(request.getEndpoint(), new ConnectCallback() { 81 | @Override 82 | public void onConnectCompleted(Exception ex, SocketIOClient client) { 83 | if (callback != null) { 84 | callback.onConnectCompleted(ex, client); 85 | } 86 | } 87 | }); 88 | } 89 | }; 90 | 91 | connection.clients.add(new SocketIOClient(connection, "", wrappedCallback)); 92 | connection.reconnect(); 93 | 94 | } 95 | 96 | ErrorCallback errorCallback; 97 | 98 | public void setErrorCallback(ErrorCallback callback) { 99 | errorCallback = callback; 100 | } 101 | 102 | public ErrorCallback getErrorCallback() { 103 | return errorCallback; 104 | } 105 | 106 | DisconnectCallback disconnectCallback; 107 | 108 | public void setDisconnectCallback(DisconnectCallback callback) { 109 | disconnectCallback = callback; 110 | } 111 | 112 | public DisconnectCallback getDisconnectCallback() { 113 | return disconnectCallback; 114 | } 115 | 116 | ReconnectCallback reconnectCallback; 117 | 118 | public void setReconnectCallback(ReconnectCallback callback) { 119 | reconnectCallback = callback; 120 | } 121 | 122 | public ReconnectCallback getReconnectCallback() { 123 | return reconnectCallback; 124 | } 125 | 126 | JSONCallback jsonCallback; 127 | 128 | public void setJSONCallback(JSONCallback callback) { 129 | jsonCallback = callback; 130 | } 131 | 132 | public JSONCallback getJSONCallback() { 133 | return jsonCallback; 134 | } 135 | 136 | StringCallback stringCallback; 137 | 138 | public void setStringCallback(StringCallback callback) { 139 | stringCallback = callback; 140 | } 141 | 142 | public StringCallback getStringCallback() { 143 | return stringCallback; 144 | } 145 | 146 | SocketIOConnection connection; 147 | String endpoint; 148 | 149 | private SocketIOClient(SocketIOConnection connection, String endpoint, 150 | ConnectCallback callback) { 151 | this.endpoint = endpoint; 152 | this.connection = connection; 153 | this.connectCallback = callback; 154 | } 155 | 156 | public boolean isConnected() { 157 | return connected && !disconnected && connection.isConnected(); 158 | } 159 | 160 | public void disconnect() { 161 | connection.disconnect(this); 162 | final DisconnectCallback disconnectCallback = this.disconnectCallback; 163 | if (disconnectCallback != null) { 164 | handler.post(new Runnable() { 165 | 166 | @Override 167 | public void run() { 168 | disconnectCallback.onDisconnect(null); 169 | 170 | } 171 | }); 172 | 173 | } 174 | } 175 | 176 | public void of(String endpoint, ConnectCallback connectCallback) { 177 | connection.connect(new SocketIOClient(connection, endpoint, connectCallback)); 178 | } 179 | 180 | public WebSocketClient getWebSocket() { 181 | return connection.webSocketClient; 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/SocketIOConnection.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.HashSet; 8 | import java.util.Hashtable; 9 | 10 | import org.json.JSONArray; 11 | import org.json.JSONObject; 12 | 13 | import android.net.Uri; 14 | import android.os.Handler; 15 | import android.text.TextUtils; 16 | 17 | import com.codebutler.android_websockets.WebSocketClient; 18 | import com.codebutler.android_websockets.WebSocketClient.Listener; 19 | import com.koushikdutta.http.AsyncHttpClient; 20 | import com.koushikdutta.http.AsyncHttpClient.SocketIORequest; 21 | 22 | /** 23 | * Created by koush on 7/1/13. 24 | */ 25 | class SocketIOConnection { 26 | 27 | private Handler mHandler; 28 | AsyncHttpClient httpClient; 29 | int heartbeat; 30 | ArrayList clients = new ArrayList(); 31 | WebSocketClient webSocketClient; 32 | SocketIORequest request; 33 | 34 | public SocketIOConnection(Handler handler, AsyncHttpClient httpClient, 35 | SocketIORequest request) { 36 | mHandler = handler; 37 | this.httpClient = httpClient; 38 | this.request = request; 39 | } 40 | 41 | public boolean isConnected() { 42 | return webSocketClient != null && webSocketClient.isConnected(); 43 | } 44 | 45 | Hashtable acknowledges = new Hashtable(); 46 | int ackCount; 47 | 48 | public void emitRaw(int type, SocketIOClient client, String message, Acknowledge acknowledge) { 49 | String ack = ""; 50 | if (acknowledge != null) { 51 | String id = "" + ackCount++; 52 | ack = id + "+"; 53 | acknowledges.put(id, acknowledge); 54 | } 55 | webSocketClient.send(String.format("%d:%s:%s:%s", type, ack, client.endpoint, message)); 56 | } 57 | 58 | public void connect(SocketIOClient client) { 59 | clients.add(client); 60 | webSocketClient.send(String.format("1::%s", client.endpoint)); 61 | } 62 | 63 | public void disconnect(SocketIOClient client) { 64 | clients.remove(client); 65 | 66 | // see if we can leave this endpoint completely 67 | boolean needsEndpointDisconnect = true; 68 | for (SocketIOClient other : clients) { 69 | // if this is the default endpoint (which disconnects everything), 70 | // or another client is using this endpoint, 71 | // we can't disconnect 72 | if (TextUtils.equals(other.endpoint, client.endpoint) 73 | || TextUtils.isEmpty(client.endpoint)) { 74 | needsEndpointDisconnect = false; 75 | break; 76 | } 77 | } 78 | 79 | if (needsEndpointDisconnect) 80 | webSocketClient.send(String.format("0::%s", client.endpoint)); 81 | 82 | // and see if we can disconnect the socket completely 83 | if (clients.size() > 0) 84 | return; 85 | 86 | webSocketClient.disconnect(); 87 | webSocketClient = null; 88 | } 89 | 90 | void reconnect() { 91 | if (isConnected()) { 92 | return; 93 | } 94 | 95 | // initiate a session 96 | httpClient.executeString(request, new AsyncHttpClient.StringCallback() { 97 | @Override 98 | public void onCompleted(final Exception e, String result) { 99 | if (e != null) { 100 | reportDisconnect(e); 101 | return; 102 | } 103 | 104 | try { 105 | String[] parts = result.split(":"); 106 | String session = parts[0]; 107 | if (!"".equals(parts[1])) 108 | heartbeat = Integer.parseInt(parts[1]) / 2 * 1000; 109 | else 110 | heartbeat = 0; 111 | 112 | String transportsLine = parts[3]; 113 | String[] transports = transportsLine.split(","); 114 | HashSet set = new HashSet(Arrays.asList(transports)); 115 | if (!set.contains("websocket")) 116 | throw new Exception("websocket not supported"); 117 | 118 | final String sessionUrl = Uri.parse(request.getUri()).buildUpon() 119 | .appendPath("websocket").appendPath(session) 120 | .build().toString(); 121 | 122 | SocketIOConnection.this.webSocketClient = new WebSocketClient(URI.create(sessionUrl), new Listener() { 123 | 124 | @Override 125 | public void onMessage(byte[] data) { 126 | //Do nothing 127 | 128 | } 129 | 130 | @Override 131 | public void onMessage(String message) { 132 | try { 133 | // Log.d(TAG, "Message: " + message); 134 | String[] parts = message.split(":", 4); 135 | int code = Integer.parseInt(parts[0]); 136 | switch (code) { 137 | case 0: 138 | // disconnect 139 | webSocketClient.disconnect(); 140 | reportDisconnect(null); 141 | break; 142 | case 1: 143 | // connect 144 | reportConnect(parts[2]); 145 | break; 146 | case 2: 147 | // heartbeat 148 | webSocketClient.send("2::"); 149 | break; 150 | case 3: { 151 | // message 152 | reportString(parts[2], parts[3], acknowledge(parts[1])); 153 | break; 154 | } 155 | case 4: { 156 | // json message 157 | final String dataString = parts[3]; 158 | final JSONObject jsonMessage = new JSONObject(dataString); 159 | reportJson(parts[2], jsonMessage, acknowledge(parts[1])); 160 | break; 161 | } 162 | case 5: { 163 | final String dataString = parts[3]; 164 | final JSONObject data = new JSONObject(dataString); 165 | final String event = data.getString("name"); 166 | final JSONArray args = data.optJSONArray("args"); 167 | reportEvent(parts[2], event, args, acknowledge(parts[1])); 168 | break; 169 | } 170 | case 6: 171 | // ACK 172 | final String[] ackParts = parts[3].split("\\+", 2); 173 | Acknowledge ack = acknowledges.remove(ackParts[0]); 174 | if (ack == null) 175 | return; 176 | JSONArray arguments = null; 177 | if (ackParts.length == 2) 178 | arguments = new JSONArray(ackParts[1]); 179 | ack.acknowledge(arguments); 180 | break; 181 | case 7: 182 | // error 183 | reportError(parts[2], parts[3]); 184 | break; 185 | case 8: 186 | // noop 187 | break; 188 | default: 189 | throw new Exception("unknown code"); 190 | } 191 | } catch (Exception ex) { 192 | webSocketClient.disconnect(); 193 | webSocketClient = null; 194 | reportDisconnect(ex); 195 | } 196 | 197 | 198 | } 199 | 200 | @Override 201 | public void onError(Exception error) { 202 | reportDisconnect(error); 203 | } 204 | 205 | @Override 206 | public void onDisconnect(int code, String reason) { 207 | 208 | reportDisconnect(new IOException(String.format("Disconnected code %d for reason %s", code, reason))); 209 | } 210 | 211 | @Override 212 | public void onConnect() { 213 | reconnectDelay = 1000L; 214 | setupHeartbeat(); 215 | 216 | } 217 | }, null); 218 | SocketIOConnection.this.webSocketClient.connect(); 219 | 220 | } catch (Exception ex) { 221 | reportDisconnect(ex); 222 | } 223 | } 224 | }); 225 | 226 | } 227 | 228 | void setupHeartbeat() { 229 | final WebSocketClient ws = webSocketClient; 230 | Runnable heartbeatRunner = new Runnable() { 231 | @Override 232 | public void run() { 233 | if (heartbeat <= 0 || ws != webSocketClient || ws == null 234 | || !ws.isConnected()) 235 | return; 236 | webSocketClient.send("2:::"); 237 | 238 | mHandler.postDelayed(this, heartbeat); 239 | } 240 | }; 241 | heartbeatRunner.run(); 242 | } 243 | 244 | private interface SelectCallback { 245 | void onSelect(SocketIOClient client); 246 | } 247 | 248 | private void select(String endpoint, SelectCallback callback) { 249 | for (SocketIOClient client : clients) { 250 | if (endpoint == null || TextUtils.equals(client.endpoint, endpoint)) { 251 | callback.onSelect(client); 252 | } 253 | } 254 | } 255 | 256 | private void delayReconnect() { 257 | if (webSocketClient != null || clients.size() == 0) 258 | return; 259 | 260 | // see if any client has disconnected, 261 | // and that we need a reconnect 262 | boolean disconnected = false; 263 | for (SocketIOClient client : clients) { 264 | if (client.disconnected) { 265 | disconnected = true; 266 | break; 267 | } 268 | } 269 | 270 | if (!disconnected) 271 | return; 272 | 273 | mHandler.postDelayed(new Runnable() { 274 | @Override 275 | public void run() { 276 | reconnect(); 277 | } 278 | }, reconnectDelay); 279 | reconnectDelay *= 2; 280 | } 281 | 282 | long reconnectDelay = 1000L; 283 | 284 | private void reportDisconnect(final Exception ex) { 285 | select(null, new SelectCallback() { 286 | @Override 287 | public void onSelect(final SocketIOClient client) { 288 | if (client.connected) { 289 | client.disconnected = true; 290 | final DisconnectCallback closed = client.getDisconnectCallback(); 291 | if (closed != null) { 292 | mHandler.post(new Runnable() { 293 | 294 | @Override 295 | public void run() { 296 | closed.onDisconnect(ex); 297 | 298 | } 299 | }); 300 | 301 | } 302 | } else { 303 | // client has never connected, this is a initial connect 304 | // failure 305 | final ConnectCallback callback = client.connectCallback; 306 | if (callback != null) { 307 | mHandler.post(new Runnable() { 308 | 309 | @Override 310 | public void run() { 311 | callback.onConnectCompleted(ex, client); 312 | 313 | } 314 | }); 315 | 316 | } 317 | } 318 | } 319 | }); 320 | 321 | delayReconnect(); 322 | } 323 | 324 | private void reportConnect(String endpoint) { 325 | select(endpoint, new SelectCallback() { 326 | @Override 327 | public void onSelect(SocketIOClient client) { 328 | if (client.isConnected()) 329 | return; 330 | if (!client.connected) { 331 | // normal connect 332 | client.connected = true; 333 | ConnectCallback callback = client.connectCallback; 334 | if (callback != null) 335 | callback.onConnectCompleted(null, client); 336 | } else if (client.disconnected) { 337 | // reconnect 338 | client.disconnected = false; 339 | ReconnectCallback callback = client.reconnectCallback; 340 | if (callback != null) 341 | callback.onReconnect(); 342 | } else { 343 | // double connect? 344 | // assert false; 345 | } 346 | } 347 | }); 348 | } 349 | 350 | private void reportJson(String endpoint, final JSONObject jsonMessage, final Acknowledge acknowledge) { 351 | select(endpoint, new SelectCallback() { 352 | @Override 353 | public void onSelect(SocketIOClient client) { 354 | final JSONCallback callback = client.jsonCallback; 355 | if (callback != null) { 356 | mHandler.post(new Runnable() { 357 | 358 | @Override 359 | public void run() { 360 | callback.onJSON(jsonMessage, acknowledge); 361 | 362 | } 363 | }); 364 | } 365 | 366 | } 367 | }); 368 | } 369 | 370 | private void reportString(String endpoint, final String string, final Acknowledge acknowledge) { 371 | select(endpoint, new SelectCallback() { 372 | @Override 373 | public void onSelect(SocketIOClient client) { 374 | final StringCallback callback = client.stringCallback; 375 | if (callback != null) { 376 | mHandler.post(new Runnable() { 377 | 378 | @Override 379 | public void run() { 380 | callback.onString(string, acknowledge); 381 | 382 | } 383 | }); 384 | 385 | } 386 | } 387 | }); 388 | } 389 | 390 | private void reportEvent(String endpoint, final String event, final JSONArray arguments, final Acknowledge acknowledge) { 391 | select(endpoint, new SelectCallback() { 392 | @Override 393 | public void onSelect(final SocketIOClient client) { 394 | mHandler.post(new Runnable() { 395 | 396 | @Override 397 | public void run() { 398 | client.onEvent(event, arguments, acknowledge); 399 | 400 | } 401 | }); 402 | 403 | } 404 | }); 405 | } 406 | 407 | private void reportError(String endpoint, final String error) { 408 | select(endpoint, new SelectCallback() { 409 | @Override 410 | public void onSelect(SocketIOClient client) { 411 | final ErrorCallback callback = client.errorCallback; 412 | if (callback != null) { 413 | 414 | mHandler.post(new Runnable() { 415 | 416 | @Override 417 | public void run() { 418 | callback.onError(error); 419 | 420 | } 421 | }); 422 | 423 | } 424 | } 425 | }); 426 | } 427 | 428 | private Acknowledge acknowledge(final String messageId) { 429 | if (TextUtils.isEmpty(messageId)) 430 | return null; 431 | 432 | return new Acknowledge() { 433 | @Override 434 | public void acknowledge(JSONArray arguments) { 435 | String data = ""; 436 | if (arguments != null) 437 | data += "+" + arguments.toString(); 438 | webSocketClient.send(String.format("6:::%s%s", messageId, data)); 439 | } 440 | }; 441 | } 442 | 443 | 444 | 445 | } 446 | -------------------------------------------------------------------------------- /src/com/koushikdutta/async/http/socketio/StringCallback.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.http.socketio; 2 | 3 | public interface StringCallback { 4 | public void onString(String string, Acknowledge acknowledge); 5 | } -------------------------------------------------------------------------------- /src/com/koushikdutta/async/util/HashList.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.async.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Hashtable; 5 | 6 | /** 7 | * Created by koush on 5/27/13. 8 | */ 9 | public class HashList extends Hashtable> { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | public HashList() { 14 | } 15 | 16 | public boolean contains(String key) { 17 | ArrayList check = get(key); 18 | return check != null && check.size() > 0; 19 | } 20 | 21 | public void add(String key, T value) { 22 | ArrayList ret = get(key); 23 | if (ret == null) { 24 | ret = new ArrayList(); 25 | put(key, ret); 26 | } 27 | ret.add(value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/com/koushikdutta/http/AsyncHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.koushikdutta.http; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.DataInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Map.Entry; 11 | 12 | import org.apache.http.HttpRequest; 13 | import org.apache.http.HttpResponse; 14 | import org.apache.http.client.methods.HttpPost; 15 | import org.apache.http.message.BasicNameValuePair; 16 | 17 | import android.net.Uri; 18 | import android.net.http.AndroidHttpClient; 19 | import android.os.AsyncTask; 20 | 21 | import com.codebutler.android_websockets.WebSocketClient; 22 | 23 | /** 24 | * 25 | * Created by Vinay S Shenoy on 07/09/2013 26 | */ 27 | public class AsyncHttpClient { 28 | 29 | public AsyncHttpClient() { 30 | 31 | } 32 | 33 | public static class SocketIORequest { 34 | 35 | private String mUri; 36 | private String mEndpoint; 37 | private List mHeaders; 38 | 39 | public SocketIORequest(String uri) { 40 | this(uri, null); 41 | } 42 | 43 | public SocketIORequest(String uri, String endpoint) { 44 | this(uri, endpoint, null); 45 | } 46 | 47 | public SocketIORequest(String uri, String endpoint, List headers) { 48 | mUri = Uri.parse(uri).buildUpon().encodedPath("/socket.io/1/").build().toString(); 49 | mEndpoint = endpoint; 50 | mHeaders = headers; 51 | } 52 | 53 | public String getUri() { 54 | return mUri; 55 | } 56 | 57 | public String getEndpoint() { 58 | return mEndpoint; 59 | } 60 | 61 | public List getHeaders() { 62 | return mHeaders; 63 | } 64 | } 65 | 66 | public static interface StringCallback { 67 | public void onCompleted(final Exception e, String result); 68 | } 69 | 70 | public static interface WebSocketConnectCallback { 71 | public void onCompleted(Exception ex, WebSocketClient webSocket); 72 | } 73 | 74 | public void executeString(final SocketIORequest socketIORequest, final StringCallback stringCallback) { 75 | 76 | new AsyncTask() { 77 | 78 | @Override 79 | protected Void doInBackground(Void... params) { 80 | 81 | AndroidHttpClient httpClient = AndroidHttpClient.newInstance("android-websockets-2.0"); 82 | HttpPost post = new HttpPost(socketIORequest.getUri()); 83 | addHeadersToRequest(post, socketIORequest.getHeaders()); 84 | 85 | try { 86 | HttpResponse res = httpClient.execute(post); 87 | String responseString = readToEnd(res.getEntity().getContent()); 88 | 89 | if (stringCallback != null) { 90 | stringCallback.onCompleted(null, responseString); 91 | } 92 | 93 | } catch (IOException e) { 94 | 95 | if (stringCallback != null) { 96 | stringCallback.onCompleted(e, null); 97 | } 98 | } finally { 99 | httpClient.close(); 100 | httpClient = null; 101 | } 102 | return null; 103 | } 104 | 105 | private void addHeadersToRequest(HttpRequest request, List headers) { 106 | if (headers != null) { 107 | Iterator it = headers.iterator(); 108 | while (it.hasNext()) { 109 | BasicNameValuePair header = it.next(); 110 | request.addHeader(header.getName(), header.getValue()); 111 | } 112 | } 113 | } 114 | }.execute(); 115 | } 116 | 117 | private byte[] readToEndAsArray(InputStream input) throws IOException { 118 | DataInputStream dis = new DataInputStream(input); 119 | byte[] stuff = new byte[1024]; 120 | ByteArrayOutputStream buff = new ByteArrayOutputStream(); 121 | int read = 0; 122 | while ((read = dis.read(stuff)) != -1) { 123 | buff.write(stuff, 0, read); 124 | } 125 | 126 | return buff.toByteArray(); 127 | } 128 | 129 | private String readToEnd(InputStream input) throws IOException { 130 | return new String(readToEndAsArray(input)); 131 | } 132 | 133 | } 134 | --------------------------------------------------------------------------------