├── .gitignore ├── AndroidManifest.xml ├── README.md ├── android-websockets.iml ├── ant.properties ├── build.xml ├── proguard-project.txt ├── project.properties └── src └── com └── codebutler └── android_websockets ├── HybiParser.java ├── SocketIOClient.java └── WebSocketClient.java /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | .classpath 4 | .project 5 | local.properties 6 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket and Socket.IO client for Android 2 | 3 | A very simple bare-minimum WebSocket and Socket.IO client for Android. 4 | 5 | ## Credits 6 | 7 | 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! 8 | 9 | The hybi parser was ported from JavaScript to Java by [Eric Butler](https://twitter.com/codebutler) . 10 | 11 | The WebSocket client was written by [Eric Butler](https://twitter.com/codebutler) . 12 | 13 | The Socket.IO client was written by [Koushik Dutta](https://twitter.com/koush). 14 | 15 | ## WebSocket Usage 16 | 17 | ```java 18 | List extraHeaders = Arrays.asList( 19 | new BasicNameValuePair("Cookie", "session=abcd"); 20 | ); 21 | 22 | WebSocketClient client = new WebSocketClient(URI.create("wss://irccloud.com"), new WebSocketClient.Handler() { 23 | @Override 24 | public void onConnect() { 25 | Log.d(TAG, "Connected!"); 26 | } 27 | 28 | @Override 29 | public void onMessage(String message) { 30 | Log.d(TAG, String.format("Got string message! %s", message)); 31 | } 32 | 33 | @Override 34 | public void onMessage(byte[] data) { 35 | Log.d(TAG, String.format("Got binary message! %s", toHexString(data)); 36 | } 37 | 38 | @Override 39 | public void onDisconnect(int code, String reason) { 40 | Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason)); 41 | } 42 | 43 | @Override 44 | public void onError(Exception error) { 45 | Log.e(TAG, "Error!", error); 46 | } 47 | }, extraHeaders); 48 | 49 | client.connect(); 50 | 51 | // Later… 52 | client.send("hello!"); 53 | client.send(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }); 54 | client.disconnect(); 55 | ``` 56 | 57 | ## Socket.IO Usage 58 | 59 | ```java 60 | SocketIOClient client = new SocketIOClient(URI.create("wss://example.com"), new SocketIOClient.Handler() { 61 | @Override 62 | public void onConnect() { 63 | Log.d(TAG, "Connected!"); 64 | } 65 | 66 | @Override 67 | public void on(String event, JSONArray arguments) { 68 | Log.d(TAG, String.format("Got event %s: %s", event, arguments.toString())); 69 | } 70 | 71 | @Override 72 | public void onJSON(JSONObject json) { 73 | try { 74 | Log.d(TAG, String.format("Got JSON Object: %s", json.toString())); 75 | } catch(JSONException e) { 76 | } 77 | } 78 | 79 | @Override 80 | public void onMessage(String message) { 81 | Log.d(TAG, String.format("Got message: %s", message)); 82 | } 83 | 84 | @Override 85 | public void onDisconnect(int code, String reason) { 86 | Log.d(TAG, String.format("Disconnected! Code: %d Reason: %s", code, reason)); 87 | } 88 | 89 | @Override 90 | public void onError(Exception error) { 91 | Log.e(TAG, "Error!", error); 92 | } 93 | }); 94 | 95 | client.connect(); 96 | 97 | // Later… 98 | client.emit("Message"); //Message 99 | JSONArray arguments = new JSONArray(); 100 | arguments.put("first argument"); 101 | JSONObject second = new JSONObject(); 102 | second.put("dictionary", true); 103 | client.emit(second); //JSON Message 104 | arguments.put(second); 105 | client.emit("hello", arguments); //Event 106 | client.disconnect(); 107 | ``` 108 | 109 | 110 | 111 | ## TODO 112 | 113 | * Run [autobahn tests](http://autobahn.ws/testsuite) 114 | * Investigate using [naga](http://code.google.com/p/naga/) instead of threads. 115 | 116 | ## License 117 | 118 | (The MIT License) 119 | 120 | Copyright (c) 2009-2012 James Coglan 121 | Copyright (c) 2012 Eric Butler 122 | Copyright (c) 2012 Koushik Dutta 123 | 124 | Permission is hereby granted, free of charge, to any person obtaining a copy of 125 | this software and associated documentation files (the 'Software'), to deal in 126 | the Software without restriction, including without limitation the rights to use, 127 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 128 | Software, and to permit persons to whom the Software is furnished to do so, 129 | subject to the following conditions: 130 | 131 | The above copyright notice and this permission notice shall be included in all 132 | copies or substantial portions of the Software. 133 | 134 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 135 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 136 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 137 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 138 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 139 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 140 | 141 | -------------------------------------------------------------------------------- /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.os.PowerManager.WakeLock; 34 | import android.util.Log; 35 | 36 | import java.io.*; 37 | import java.util.Arrays; 38 | import java.util.List; 39 | 40 | public class HybiParser { 41 | private static final String TAG = "HybiParser"; 42 | 43 | private WebSocketClient mClient; 44 | 45 | private WakeLock mWakeLock; 46 | 47 | private boolean mMasking = true; 48 | 49 | private int mStage; 50 | 51 | private boolean mFinal; 52 | private boolean mMasked; 53 | private int mOpcode; 54 | private int mLengthSize; 55 | private int mLength; 56 | private int mMode; 57 | 58 | private byte[] mMask = new byte[0]; 59 | private byte[] mPayload = new byte[0]; 60 | 61 | private boolean mClosed = false; 62 | 63 | private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); 64 | 65 | private static final int BYTE = 255; 66 | private static final int FIN = 128; 67 | private static final int MASK = 128; 68 | private static final int RSV1 = 64; 69 | private static final int RSV2 = 32; 70 | private static final int RSV3 = 16; 71 | private static final int OPCODE = 15; 72 | private static final int LENGTH = 127; 73 | 74 | private static final int MODE_TEXT = 1; 75 | private static final int MODE_BINARY = 2; 76 | 77 | private static final int OP_CONTINUATION = 0; 78 | private static final int OP_TEXT = 1; 79 | private static final int OP_BINARY = 2; 80 | private static final int OP_CLOSE = 8; 81 | private static final int OP_PING = 9; 82 | private static final int OP_PONG = 10; 83 | 84 | private static final List OPCODES = Arrays.asList( 85 | OP_CONTINUATION, 86 | OP_TEXT, 87 | OP_BINARY, 88 | OP_CLOSE, 89 | OP_PING, 90 | OP_PONG 91 | ); 92 | 93 | private static final List FRAGMENTED_OPCODES = Arrays.asList( 94 | OP_CONTINUATION, OP_TEXT, OP_BINARY 95 | ); 96 | 97 | public HybiParser(WebSocketClient client) { 98 | mClient = client; 99 | } 100 | 101 | public HybiParser(WebSocketClient webSocketClient, WakeLock wakelock) { 102 | mWakeLock = wakelock; 103 | mClient = webSocketClient; 104 | } 105 | 106 | private static byte[] mask(byte[] payload, byte[] mask, int offset) { 107 | if (mask.length == 0) return payload; 108 | 109 | for (int i = 0; i < payload.length - offset; i++) { 110 | payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]); 111 | } 112 | return payload; 113 | } 114 | 115 | public void start(HappyDataInputStream stream) throws IOException { 116 | while (true) { 117 | if (stream.available() == -1) break; 118 | switch (mStage) { 119 | case 0: 120 | parseOpcode(stream.readByte()); 121 | break; 122 | case 1: 123 | parseLength(stream.readByte()); 124 | break; 125 | case 2: 126 | parseExtendedLength(stream.readBytes(mLengthSize)); 127 | break; 128 | case 3: 129 | mMask = stream.readBytes(4); 130 | mStage = 4; 131 | break; 132 | case 4: 133 | mPayload = stream.readBytes(mLength); 134 | emitFrame(); 135 | mStage = 0; 136 | break; 137 | } 138 | if(mWakeLock != null && mFinal) synchronized (mWakeLock) { 139 | if(mWakeLock.isHeld())mWakeLock.release(); 140 | } 141 | } 142 | mClient.getListener().onDisconnect(0, "EOF"); 143 | } 144 | 145 | private void parseOpcode(byte data) throws ProtocolError { 146 | 147 | if(mWakeLock != null) synchronized (mWakeLock) { 148 | mWakeLock.acquire(); 149 | } 150 | 151 | boolean rsv1 = (data & RSV1) == RSV1; 152 | boolean rsv2 = (data & RSV2) == RSV2; 153 | boolean rsv3 = (data & RSV3) == RSV3; 154 | 155 | if (rsv1 || rsv2 || rsv3) { 156 | throw new ProtocolError("RSV not zero"); 157 | } 158 | 159 | mFinal = (data & FIN) == FIN; 160 | mOpcode = (data & OPCODE); 161 | mMask = new byte[0]; 162 | mPayload = new byte[0]; 163 | 164 | if (!OPCODES.contains(mOpcode)) { 165 | throw new ProtocolError("Bad opcode"); 166 | } 167 | 168 | if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) { 169 | throw new ProtocolError("Expected non-final packet"); 170 | } 171 | 172 | mStage = 1; 173 | } 174 | 175 | private void parseLength(byte data) { 176 | mMasked = (data & MASK) == MASK; 177 | mLength = (data & LENGTH); 178 | 179 | if (mLength >= 0 && mLength <= 125) { 180 | mStage = mMasked ? 3 : 4; 181 | } else { 182 | mLengthSize = (mLength == 126) ? 2 : 8; 183 | mStage = 2; 184 | } 185 | } 186 | 187 | private void parseExtendedLength(byte[] buffer) throws ProtocolError { 188 | mLength = getInteger(buffer); 189 | mStage = mMasked ? 3 : 4; 190 | } 191 | 192 | public byte[] frame(String data) { 193 | return frame(data, OP_TEXT, -1); 194 | } 195 | 196 | public byte[] frame(byte[] data) { 197 | return frame(data, OP_BINARY, -1); 198 | } 199 | 200 | private byte[] frame(byte[] data, int opcode, int errorCode) { 201 | return frame((Object)data, opcode, errorCode); 202 | } 203 | 204 | private byte[] frame(String data, int opcode, int errorCode) { 205 | return frame((Object)data, opcode, errorCode); 206 | } 207 | 208 | private byte[] frame(Object data, int opcode, int errorCode) { 209 | if (mClosed) return null; 210 | 211 | Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode); 212 | 213 | byte[] buffer = (data instanceof String) ? decode((String) data) : (byte[]) data; 214 | int insert = (errorCode > 0) ? 2 : 0; 215 | int length = buffer.length + insert; 216 | int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10); 217 | int offset = header + (mMasking ? 4 : 0); 218 | int masked = mMasking ? MASK : 0; 219 | byte[] frame = new byte[length + offset]; 220 | 221 | frame[0] = (byte) ((byte)FIN | (byte)opcode); 222 | 223 | if (length <= 125) { 224 | frame[1] = (byte) (masked | length); 225 | } else if (length <= 65535) { 226 | frame[1] = (byte) (masked | 126); 227 | frame[2] = (byte) Math.floor(length / 256); 228 | frame[3] = (byte) (length & BYTE); 229 | } else { 230 | frame[1] = (byte) (masked | 127); 231 | frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE); 232 | frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE); 233 | frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE); 234 | frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE); 235 | frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE); 236 | frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE); 237 | frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE); 238 | frame[9] = (byte) (length & BYTE); 239 | } 240 | 241 | if (errorCode > 0) { 242 | frame[offset] = (byte) (((int) Math.floor(errorCode / 256)) & BYTE); 243 | frame[offset+1] = (byte) (errorCode & BYTE); 244 | } 245 | System.arraycopy(buffer, 0, frame, offset + insert, buffer.length); 246 | 247 | if (mMasking) { 248 | byte[] mask = { 249 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256), 250 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256) 251 | }; 252 | System.arraycopy(mask, 0, frame, header, mask.length); 253 | mask(frame, mask, offset); 254 | } 255 | 256 | return frame; 257 | } 258 | 259 | public void ping(String message) { 260 | mClient.send(frame(message, OP_PING, -1)); 261 | } 262 | 263 | public void close(int code, String reason) { 264 | if (mClosed) return; 265 | mClient.send(frame(reason, OP_CLOSE, code)); 266 | mClosed = true; 267 | } 268 | 269 | private void emitFrame() throws IOException { 270 | byte[] payload = mask(mPayload, mMask, 0); 271 | int opcode = mOpcode; 272 | 273 | if (opcode == OP_CONTINUATION) { 274 | if (mMode == 0) { 275 | throw new ProtocolError("Mode was not set."); 276 | } 277 | mBuffer.write(payload); 278 | if (mFinal) { 279 | byte[] message = mBuffer.toByteArray(); 280 | if (mMode == MODE_TEXT) { 281 | mClient.getListener().onMessage(encode(message)); 282 | } else { 283 | mClient.getListener().onMessage(message); 284 | } 285 | reset(); 286 | } 287 | 288 | } else if (opcode == OP_TEXT) { 289 | if (mFinal) { 290 | String messageText = encode(payload); 291 | mClient.getListener().onMessage(messageText); 292 | } else { 293 | mMode = MODE_TEXT; 294 | mBuffer.write(payload); 295 | } 296 | 297 | } else if (opcode == OP_BINARY) { 298 | if (mFinal) { 299 | mClient.getListener().onMessage(payload); 300 | } else { 301 | mMode = MODE_BINARY; 302 | mBuffer.write(payload); 303 | } 304 | 305 | } else if (opcode == OP_CLOSE) { 306 | int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0; 307 | String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null; 308 | Log.d(TAG, "Got close op! " + code + " " + reason); 309 | mClient.getListener().onDisconnect(code, reason); 310 | 311 | } else if (opcode == OP_PING) { 312 | if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); } 313 | Log.d(TAG, "Sending pong!!"); 314 | mClient.sendFrame(frame(payload, OP_PONG, -1)); 315 | 316 | } else if (opcode == OP_PONG) { 317 | String message = encode(payload); 318 | // FIXME: Fire callback... 319 | Log.d(TAG, "Got pong! " + message); 320 | } 321 | } 322 | 323 | private void reset() { 324 | mMode = 0; 325 | mBuffer.reset(); 326 | } 327 | 328 | private String encode(byte[] buffer) { 329 | try { 330 | return new String(buffer, "UTF-8"); 331 | } catch (UnsupportedEncodingException e) { 332 | throw new RuntimeException(e); 333 | } 334 | } 335 | 336 | private byte[] decode(String string) { 337 | try { 338 | return (string).getBytes("UTF-8"); 339 | } catch (UnsupportedEncodingException e) { 340 | throw new RuntimeException(e); 341 | } 342 | } 343 | 344 | private int getInteger(byte[] bytes) throws ProtocolError { 345 | long i = byteArrayToLong(bytes, 0, bytes.length); 346 | if (i < 0 || i > Integer.MAX_VALUE) { 347 | throw new ProtocolError("Bad integer: " + i); 348 | } 349 | return (int) i; 350 | } 351 | 352 | /** 353 | * Copied from AOSP Arrays.java. 354 | */ 355 | /** 356 | * Copies elements from {@code original} into a new array, from indexes start (inclusive) to 357 | * end (exclusive). The original order of elements is preserved. 358 | * If {@code end} is greater than {@code original.length}, the result is padded 359 | * with the value {@code (byte) 0}. 360 | * 361 | * @param original the original array 362 | * @param start the start index, inclusive 363 | * @param end the end index, exclusive 364 | * @return the new array 365 | * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length} 366 | * @throws IllegalArgumentException if {@code start > end} 367 | * @throws NullPointerException if {@code original == null} 368 | * @since 1.6 369 | */ 370 | private static byte[] copyOfRange(byte[] original, int start, int end) { 371 | if (start > end) { 372 | throw new IllegalArgumentException(); 373 | } 374 | int originalLength = original.length; 375 | if (start < 0 || start > originalLength) { 376 | throw new ArrayIndexOutOfBoundsException(); 377 | } 378 | int resultLength = end - start; 379 | int copyLength = Math.min(resultLength, originalLength - start); 380 | byte[] result = new byte[resultLength]; 381 | System.arraycopy(original, start, result, 0, copyLength); 382 | return result; 383 | } 384 | 385 | private byte[] slice(byte[] array, int start) { 386 | return copyOfRange(array, start, array.length); 387 | } 388 | 389 | public static class ProtocolError extends IOException { 390 | public ProtocolError(String detailMessage) { 391 | super(detailMessage); 392 | } 393 | } 394 | 395 | private static long byteArrayToLong(byte[] b, int offset, int length) { 396 | if (b.length < length) 397 | throw new IllegalArgumentException("length must be less than or equal to b.length"); 398 | 399 | long value = 0; 400 | for (int i = 0; i < length; i++) { 401 | int shift = (length - 1 - i) * 8; 402 | value += (b[i + offset] & 0x000000FF) << shift; 403 | } 404 | return value; 405 | } 406 | 407 | public static class HappyDataInputStream extends DataInputStream { 408 | public HappyDataInputStream(InputStream in) { 409 | super(in); 410 | } 411 | 412 | public byte[] readBytes(int length) throws IOException { 413 | byte[] buffer = new byte[length]; 414 | 415 | int total = 0; 416 | 417 | while (total < length) { 418 | int count = read(buffer, total, length - total); 419 | if (count == -1) { 420 | break; 421 | } 422 | total += count; 423 | } 424 | 425 | if (total != length) { 426 | throw new IOException(String.format("Read wrong number of bytes. Got: %s, Expected: %s.", total, length)); 427 | } 428 | 429 | return buffer; 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/com/codebutler/android_websockets/SocketIOClient.java: -------------------------------------------------------------------------------- 1 | package com.codebutler.android_websockets; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.DataInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import java.util.Arrays; 10 | import java.util.HashSet; 11 | 12 | import org.apache.http.HttpResponse; 13 | import org.apache.http.client.methods.HttpPost; 14 | import org.apache.http.client.methods.HttpUriRequest; 15 | import org.json.JSONArray; 16 | import org.json.JSONException; 17 | import org.json.JSONObject; 18 | 19 | import android.net.http.AndroidHttpClient; 20 | import android.os.Looper; 21 | import android.util.Log; 22 | 23 | public class SocketIOClient { 24 | public static interface Handler { 25 | public void onConnect(); 26 | 27 | public void on(String event, JSONArray arguments); 28 | 29 | public void onDisconnect(int code, String reason); 30 | 31 | public void onJSON(JSONObject json); 32 | 33 | public void onMessage(String message); 34 | 35 | public void onError(Exception error); 36 | } 37 | 38 | private static final String TAG = "SocketIOClient"; 39 | 40 | String mURL; 41 | Handler mHandler; 42 | String mSession; 43 | int mHeartbeat; 44 | WebSocketClient mClient; 45 | 46 | public SocketIOClient(URI uri, Handler handler) { 47 | // remove trailing "/" from URI, in case user provided e.g. http://test.com/ 48 | mURL = uri.toString().replaceAll("/$", "") + "/socket.io/1/"; 49 | mHandler = handler; 50 | } 51 | 52 | private static String downloadUriAsString(final HttpUriRequest req) throws IOException { 53 | AndroidHttpClient client = AndroidHttpClient.newInstance("android-websockets"); 54 | try { 55 | HttpResponse res = client.execute(req); 56 | return readToEnd(res.getEntity().getContent()); 57 | } 58 | finally { 59 | client.close(); 60 | } 61 | } 62 | 63 | private static byte[] readToEndAsArray(InputStream input) throws IOException { 64 | DataInputStream dis = new DataInputStream(input); 65 | byte[] stuff = new byte[1024]; 66 | ByteArrayOutputStream buff = new ByteArrayOutputStream(); 67 | int read = 0; 68 | while ((read = dis.read(stuff)) != -1) { 69 | buff.write(stuff, 0, read); 70 | } 71 | 72 | return buff.toByteArray(); 73 | } 74 | 75 | private static String readToEnd(InputStream input) throws IOException { 76 | return new String(readToEndAsArray(input)); 77 | } 78 | 79 | android.os.Handler mSendHandler; 80 | Looper mSendLooper; 81 | 82 | public void emit(String name, JSONArray args) throws JSONException { 83 | final JSONObject event = new JSONObject(); 84 | event.put("name", name); 85 | event.put("args", args); 86 | Log.d(TAG, "Emitting event: " + event.toString()); 87 | mSendHandler.post(new Runnable() { 88 | @Override 89 | public void run() { 90 | mClient.send(String.format("5:::%s", event.toString())); 91 | } 92 | }); 93 | } 94 | 95 | public void emit(final String message) { 96 | mSendHandler.post(new Runnable() { 97 | 98 | @Override 99 | public void run() { 100 | mClient.send(String.format("3:::%s", message)); 101 | } 102 | }); 103 | } 104 | 105 | public void emit(final JSONObject jsonMessage) { 106 | 107 | mSendHandler.post(new Runnable() { 108 | 109 | @Override 110 | public void run() { 111 | mClient.send(String.format("4:::%s", jsonMessage.toString())); 112 | } 113 | }); 114 | } 115 | 116 | private void connectSession() throws URISyntaxException { 117 | mClient = new WebSocketClient(new URI(mURL + "websocket/" + mSession), new WebSocketClient.Listener() { 118 | @Override 119 | public void onMessage(byte[] data) { 120 | cleanup(); 121 | mHandler.onError(new Exception("Unexpected binary data")); 122 | } 123 | 124 | @Override 125 | public void onMessage(String message) { 126 | try { 127 | Log.d(TAG, "Message: " + message); 128 | String[] parts = message.split(":", 4); 129 | int code = Integer.parseInt(parts[0]); 130 | switch (code) { 131 | case 1: 132 | // connect 133 | mHandler.onConnect(); 134 | break; 135 | case 2: 136 | // heartbeat 137 | mClient.send("2::"); 138 | break; 139 | case 3: { 140 | // message 141 | final String messageId = parts[1]; 142 | final String dataString = parts[3]; 143 | 144 | if(!"".equals(messageId)) { 145 | mSendHandler.post(new Runnable() { 146 | 147 | @Override 148 | public void run() { 149 | mClient.send(String.format("6:::%s", messageId)); 150 | } 151 | }); 152 | } 153 | mHandler.onMessage(dataString); 154 | break; 155 | } 156 | case 4: { 157 | //json message 158 | final String messageId = parts[1]; 159 | final String dataString = parts[3]; 160 | 161 | JSONObject jsonMessage = null; 162 | 163 | try { 164 | jsonMessage = new JSONObject(dataString); 165 | } catch(JSONException e) { 166 | jsonMessage = new JSONObject(); 167 | } 168 | if(!"".equals(messageId)) { 169 | mSendHandler.post(new Runnable() { 170 | 171 | @Override 172 | public void run() { 173 | mClient.send(String.format("6:::%s", messageId)); 174 | } 175 | }); 176 | } 177 | mHandler.onJSON(jsonMessage); 178 | break; 179 | } 180 | case 5: { 181 | final String messageId = parts[1]; 182 | final String dataString = parts[3]; 183 | JSONObject data = new JSONObject(dataString); 184 | String event = data.getString("name"); 185 | JSONArray args; 186 | try { 187 | args = data.getJSONArray("args"); 188 | } catch (JSONException e) { 189 | args = new JSONArray(); 190 | } 191 | if (!"".equals(messageId)) { 192 | mSendHandler.post(new Runnable() { 193 | @Override 194 | public void run() { 195 | mClient.send(String.format("6:::%s", messageId)); 196 | } 197 | }); 198 | } 199 | mHandler.on(event, args); 200 | break; 201 | } 202 | case 6: 203 | // ACK 204 | break; 205 | case 7: 206 | // error 207 | throw new Exception(message); 208 | case 8: 209 | // noop 210 | break; 211 | default: 212 | throw new Exception("unknown code"); 213 | } 214 | } 215 | catch (Exception ex) { 216 | cleanup(); 217 | onError(ex); 218 | } 219 | } 220 | 221 | @Override 222 | public void onError(Exception error) { 223 | cleanup(); 224 | mHandler.onError(error); 225 | } 226 | 227 | @Override 228 | public void onDisconnect(int code, String reason) { 229 | cleanup(); 230 | // attempt reconnect with same session? 231 | mHandler.onDisconnect(code, reason); 232 | } 233 | 234 | @Override 235 | public void onConnect() { 236 | mSendHandler.postDelayed(new Runnable() { 237 | @Override 238 | public void run() { 239 | mSendHandler.postDelayed(this, mHeartbeat); 240 | mClient.send("2:::"); 241 | } 242 | }, mHeartbeat); 243 | } 244 | }, null); 245 | mClient.connect(); 246 | } 247 | 248 | public void disconnect() throws IOException { 249 | cleanup(); 250 | } 251 | 252 | private void cleanup() { 253 | mClient.disconnect(); 254 | mClient = null; 255 | 256 | mSendLooper.quit(); 257 | mSendLooper = null; 258 | mSendHandler = null; 259 | } 260 | 261 | public void connect() { 262 | if (mClient != null) 263 | return; 264 | new Thread() { 265 | public void run() { 266 | HttpPost post = new HttpPost(mURL); 267 | try { 268 | String line = downloadUriAsString(post); 269 | String[] parts = line.split(":"); 270 | mSession = parts[0]; 271 | String heartbeat = parts[1]; 272 | if (!"".equals(heartbeat)) 273 | mHeartbeat = Integer.parseInt(heartbeat) / 2 * 1000; 274 | String transportsLine = parts[3]; 275 | String[] transports = transportsLine.split(","); 276 | HashSet set = new HashSet(Arrays.asList(transports)); 277 | if (!set.contains("websocket")) 278 | throw new Exception("websocket not supported"); 279 | 280 | Looper.prepare(); 281 | mSendLooper = Looper.myLooper(); 282 | mSendHandler = new android.os.Handler(); 283 | 284 | connectSession(); 285 | 286 | Looper.loop(); 287 | } 288 | catch (Exception e) { 289 | mHandler.onError(e); 290 | } 291 | }; 292 | }.start(); 293 | } 294 | } 295 | 296 | -------------------------------------------------------------------------------- /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.os.PowerManager.WakeLock; 6 | import android.text.TextUtils; 7 | import android.util.Base64; 8 | import android.util.Log; 9 | import org.apache.http.*; 10 | import org.apache.http.client.HttpResponseException; 11 | import org.apache.http.message.BasicLineParser; 12 | import org.apache.http.message.BasicNameValuePair; 13 | 14 | import javax.net.SocketFactory; 15 | import javax.net.ssl.SSLContext; 16 | import javax.net.ssl.SSLException; 17 | import javax.net.ssl.SSLSocketFactory; 18 | import javax.net.ssl.TrustManager; 19 | import java.io.EOFException; 20 | import java.io.IOException; 21 | import java.io.OutputStream; 22 | import java.io.PrintWriter; 23 | import java.net.Socket; 24 | import java.net.URI; 25 | import java.security.KeyManagementException; 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 | private WakeLock mWakeLock; 42 | 43 | private final Object mSendLock = new Object(); 44 | 45 | private static TrustManager[] sTrustManagers; 46 | 47 | public static void setTrustManagers(TrustManager[] tm) { 48 | sTrustManagers = tm; 49 | } 50 | 51 | public WebSocketClient(URI uri, Listener listener, List extraHeaders) { 52 | mURI = uri; 53 | mListener = listener; 54 | mExtraHeaders = extraHeaders; 55 | mConnected = false; 56 | mParser = new HybiParser(this); 57 | 58 | mHandlerThread = new HandlerThread("websocket-thread"); 59 | mHandlerThread.start(); 60 | mHandler = new Handler(mHandlerThread.getLooper()); 61 | } 62 | 63 | public WebSocketClient(URI uri, Listener listener, List extraHeaders, WakeLock wakelock) { 64 | mURI = uri; 65 | mListener = listener; 66 | mExtraHeaders = extraHeaders; 67 | mConnected = false; 68 | mParser = new HybiParser(this, wakelock); 69 | 70 | mHandlerThread = new HandlerThread("websocket-thread"); 71 | mHandlerThread.start(); 72 | mHandler = new Handler(mHandlerThread.getLooper()); 73 | mWakeLock = wakelock; 74 | } 75 | 76 | public Listener getListener() { 77 | return mListener; 78 | } 79 | 80 | public void connect() { 81 | if (mThread != null && mThread.isAlive()) { 82 | return; 83 | } 84 | 85 | mThread = new Thread(new Runnable() { 86 | @Override 87 | public void run() { 88 | try { 89 | if(mWakeLock != null) synchronized (mWakeLock) { 90 | mWakeLock.acquire(); 91 | } 92 | int port = (mURI.getPort() != -1) ? mURI.getPort() : ((mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? 443 : 80); 93 | 94 | String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath(); 95 | if (!TextUtils.isEmpty(mURI.getQuery())) { 96 | path += "?" + mURI.getQuery(); 97 | } 98 | 99 | String originScheme = mURI.getScheme().equals("wss") ? "https" : "http"; 100 | URI origin = new URI(originScheme, "//" + mURI.getHost(), null); 101 | 102 | SocketFactory factory = (mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? getSSLSocketFactory() : SocketFactory.getDefault(); 103 | mSocket = factory.createSocket(mURI.getHost(), port); 104 | 105 | PrintWriter out = new PrintWriter(mSocket.getOutputStream()); 106 | out.print("GET " + path + " HTTP/1.1\r\n"); 107 | out.print("Upgrade: websocket\r\n"); 108 | out.print("Connection: Upgrade\r\n"); 109 | out.print("Host: " + mURI.getHost() + "\r\n"); 110 | out.print("Origin: " + origin.toString() + "\r\n"); 111 | out.print("Sec-WebSocket-Key: " + createSecret() + "\r\n"); 112 | out.print("Sec-WebSocket-Version: 13\r\n"); 113 | if (mExtraHeaders != null) { 114 | for (NameValuePair pair : mExtraHeaders) { 115 | out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue())); 116 | } 117 | } 118 | out.print("\r\n"); 119 | out.flush(); 120 | 121 | HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream()); 122 | 123 | // Read HTTP response status line. 124 | StatusLine statusLine = parseStatusLine(readLine(stream)); 125 | if (statusLine == null) { 126 | throw new HttpException("Received no reply from server."); 127 | } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) { 128 | throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); 129 | } 130 | 131 | // Read HTTP response headers. 132 | String line; 133 | while (!TextUtils.isEmpty(line = readLine(stream))) { 134 | Header header = parseHeader(line); 135 | if (header.getName().equals("Sec-WebSocket-Accept")) { 136 | // FIXME: Verify the response... 137 | } 138 | } 139 | 140 | mListener.onConnect(); 141 | 142 | mConnected = true; 143 | if(mWakeLock != null && mWakeLock.isHeld()) mWakeLock.release(); 144 | // Now decode websocket frames. 145 | mParser.start(stream); 146 | 147 | } catch (EOFException ex) { 148 | Log.d(TAG, "WebSocket EOF!", ex); 149 | mListener.onDisconnect(0, "EOF"); 150 | mConnected = false; 151 | 152 | } catch (SSLException ex) { 153 | // Connection reset by peer 154 | Log.d(TAG, "Websocket SSL error!", ex); 155 | mListener.onDisconnect(0, "SSL"); 156 | mConnected = false; 157 | 158 | } catch (Exception ex) { 159 | mListener.onError(ex); 160 | mConnected = false; 161 | } finally { 162 | if(mWakeLock != null && mWakeLock.isHeld()){ 163 | mWakeLock.setReferenceCounted(false); 164 | mWakeLock.release(); 165 | mWakeLock.setReferenceCounted(true); 166 | } 167 | } 168 | } 169 | }); 170 | mThread.start(); 171 | } 172 | 173 | public void disconnect() { 174 | if (mSocket != null) { 175 | mHandler.post(new Runnable() { 176 | @Override 177 | public void run() { 178 | try { 179 | mSocket.close(); 180 | mSocket = null; 181 | mConnected = false; 182 | } catch (IOException ex) { 183 | Log.d(TAG, "Error while disconnecting", ex); 184 | mListener.onError(ex); 185 | } 186 | } 187 | }); 188 | } 189 | } 190 | 191 | public void send(String data) { 192 | sendFrame(mParser.frame(data)); 193 | } 194 | 195 | public void send(byte[] data) { 196 | sendFrame(mParser.frame(data)); 197 | } 198 | 199 | public boolean isConnected() { 200 | return mConnected; 201 | } 202 | 203 | private StatusLine parseStatusLine(String line) { 204 | if (TextUtils.isEmpty(line)) { 205 | return null; 206 | } 207 | return BasicLineParser.parseStatusLine(line, new BasicLineParser()); 208 | } 209 | 210 | private Header parseHeader(String line) { 211 | return BasicLineParser.parseHeader(line, new BasicLineParser()); 212 | } 213 | 214 | // Can't use BufferedReader because it buffers past the HTTP data. 215 | private String readLine(HybiParser.HappyDataInputStream reader) throws IOException { 216 | int readChar = reader.read(); 217 | if (readChar == -1) { 218 | return null; 219 | } 220 | StringBuilder string = new StringBuilder(""); 221 | while (readChar != '\n') { 222 | if (readChar != '\r') { 223 | string.append((char) readChar); 224 | } 225 | 226 | readChar = reader.read(); 227 | if (readChar == -1) { 228 | return null; 229 | } 230 | } 231 | return string.toString(); 232 | } 233 | 234 | private String createSecret() { 235 | byte[] nonce = new byte[16]; 236 | for (int i = 0; i < 16; i++) { 237 | nonce[i] = (byte) (Math.random() * 256); 238 | } 239 | return Base64.encodeToString(nonce, Base64.DEFAULT).trim(); 240 | } 241 | 242 | void sendFrame(final byte[] frame) { 243 | mHandler.post(new Runnable() { 244 | @Override 245 | public void run() { 246 | try { 247 | synchronized (mSendLock) { 248 | if(mWakeLock != null) synchronized (mWakeLock) { 249 | mWakeLock.acquire(); 250 | } 251 | OutputStream outputStream = mSocket.getOutputStream(); 252 | outputStream.write(frame); 253 | outputStream.flush(); 254 | } 255 | } catch (IOException e) { 256 | mListener.onError(e); 257 | mConnected = false; 258 | if(mWakeLock != null) synchronized (mWakeLock) { 259 | mWakeLock.setReferenceCounted(false); 260 | } 261 | } finally { 262 | if(mWakeLock != null) synchronized (mWakeLock) { 263 | if(mWakeLock.isHeld()) { 264 | mWakeLock.release(); 265 | mWakeLock.setReferenceCounted(true); 266 | } 267 | } 268 | } 269 | } 270 | }); 271 | } 272 | 273 | public interface Listener { 274 | public void onConnect(); 275 | public void onMessage(String message); 276 | public void onMessage(byte[] data); 277 | public void onDisconnect(int code, String reason); 278 | public void onError(Exception error); 279 | } 280 | 281 | private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { 282 | SSLContext context = SSLContext.getInstance("TLS"); 283 | context.init(null, sTrustManagers, null); 284 | return context.getSocketFactory(); 285 | } 286 | } 287 | --------------------------------------------------------------------------------