├── .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 |
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.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 |
--------------------------------------------------------------------------------