├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | /res-overlay
28 |
29 |
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 |
--------------------------------------------------------------------------------