├── .classpath
├── .project
├── AndroidManifest.xml
├── README
├── assets
└── Goldprice
│ ├── cordova-1.5.0.js
│ ├── index.html
│ ├── jquery-1.8.0.js
│ ├── jquery.mobile-1.1.1.min.css
│ ├── jquery.mobile-1.1.1.min.js
│ ├── jquery.websocket.js
│ ├── json2.js
│ └── main.css
├── ic_launcher-web.png
├── libs
├── cordova-1.5.0.jar
├── jackson-core-asl-1.9.7.jar
└── jackson-mapper-asl-1.9.7.jar
├── proguard-project.txt
├── project.properties
├── res
├── drawable-hdpi
│ ├── ic_action_search.png
│ └── ic_launcher.png
├── drawable-ldpi
│ └── ic_launcher.png
├── drawable-mdpi
│ ├── ic_action_search.png
│ └── ic_launcher.png
├── drawable-xhdpi
│ ├── ic_action_search.png
│ └── ic_launcher.png
├── layout
│ └── activity_main.xml
├── values
│ ├── strings.xml
│ └── styles.xml
└── xml
│ └── plugins.xml
└── src
├── com
└── moko365
│ └── android
│ └── websocket
│ ├── MainActivity.java
│ ├── WebSocketClientBinding.java
│ ├── WebSocketHolder.java
│ ├── WebsocketDroidGap.java
│ └── WebsocketHandler.java
└── de
├── .svn
├── all-wcprops
└── entries
└── tavendo
└── autobahn
├── ByteBufferInputStream.java
├── ByteBufferOutputStream.java
├── Doxygen.java
├── NoCopyByteArrayOutputStream.java
├── PrefixMap.java
├── Utf8Validator.java
├── Wamp.java
├── WampConnection.java
├── WampConnectionHandler.java
├── WampMessage.java
├── WampOptions.java
├── WampReader.java
├── WampWriter.java
├── WebSocket.java
├── WebSocketConnection.java
├── WebSocketConnectionHandler.java
├── WebSocketException.java
├── WebSocketMessage.java
├── WebSocketOptions.java
├── WebSocketReader.java
└── WebSocketWriter.java
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | AndroidWebsocket
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | Jollen's HTML5 Course Series
2 | ----------------------------
3 |
4 | Since Android WebView doesn't support WebSocket client, HTML5 Apps using Web
5 | View aren't able to connect to WebSocket server.
6 |
7 | android-browser-websocket is a simple to implement WebSocket for WebView. It
8 | uses Autobahn WebSocket library, please refer to http://autobahn.ws first.
9 |
10 | ***
11 |
12 | Please see www.moko365.com for more course information.
13 |
--------------------------------------------------------------------------------
/assets/Goldprice/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | No Chat
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
25 |
26 |
29 |
30 |
31 |
32 |
81 |
82 |
--------------------------------------------------------------------------------
/assets/Goldprice/jquery.websocket.js:
--------------------------------------------------------------------------------
1 | (function($) {
2 |
3 | var ws; // WebSocket instance and close in this module
4 |
5 | $.fn.handleMessage = function () {
6 |
7 | var content = this;
8 | var count = 0;
9 | var room = $("#room");
10 |
11 | ws.onmessage = function (evt)
12 | {
13 | var json = JSON.parse(evt.data);
14 |
15 | content.fadeOut("slow");
16 | if (json.type === 'gold') {
17 | count++;
18 | content.html('' + json.data.timestamp + '
bid: ' + json.data.bid + '
ask: ' + json.data.ask + '
');
19 | }
20 | content.fadeIn("slow");
21 |
22 | if (json.data.message != ".") {
23 | room.prepend('' + json.data.message + '
');
24 | }
25 | };
26 |
27 | ws.connect();
28 | };
29 |
30 |
31 | $.fn.createWebSocket = function () {
32 |
33 | var content = this;
34 |
35 | if ("WebSocket" in window)
36 | {
37 | // Let us open a web socket
38 | ws = new WebSocket("ws://svn.moko365.com:8080/", "echo-protocol");
39 |
40 | ws.onopen = function(evt)
41 | {
42 | content.html("Websocket connected.
");
43 | };
44 | ws.onclose = function(evt)
45 | {
46 | content.html("Websocket connected.");
47 | };
48 | }
49 | else
50 | {
51 | // The browser doesn't support WebSocket
52 | ws = new WebSocketImpl("ws://svn.moko365.com:8080/", "echo-protocol");
53 |
54 | ws.onopen = function(evt)
55 | {
56 | content.html("Websocket connected.
");
57 | };
58 |
59 | ws.onclose = function(evt)
60 | {
61 | content.html("Websocket closed.
");
62 | };
63 |
64 | ws.onerror = function(evt)
65 | {
66 | content.html("Websocket error.
");
67 | };
68 | }
69 |
70 | };
71 |
72 | }) ($);
73 |
--------------------------------------------------------------------------------
/assets/Goldprice/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | json2.js
3 | 2011-10-19
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, regexp: true */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | var JSON;
163 | if (!JSON) {
164 | JSON = {};
165 | }
166 |
167 | (function () {
168 | 'use strict';
169 |
170 | function f(n) {
171 | // Format integers to have at least two digits.
172 | return n < 10 ? '0' + n : n;
173 | }
174 |
175 | if (typeof Date.prototype.toJSON !== 'function') {
176 |
177 | Date.prototype.toJSON = function (key) {
178 |
179 | return isFinite(this.valueOf())
180 | ? this.getUTCFullYear() + '-' +
181 | f(this.getUTCMonth() + 1) + '-' +
182 | f(this.getUTCDate()) + 'T' +
183 | f(this.getUTCHours()) + ':' +
184 | f(this.getUTCMinutes()) + ':' +
185 | f(this.getUTCSeconds()) + 'Z'
186 | : null;
187 | };
188 |
189 | String.prototype.toJSON =
190 | Number.prototype.toJSON =
191 | Boolean.prototype.toJSON = function (key) {
192 | return this.valueOf();
193 | };
194 | }
195 |
196 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
198 | gap,
199 | indent,
200 | meta = { // table of character substitutions
201 | '\b': '\\b',
202 | '\t': '\\t',
203 | '\n': '\\n',
204 | '\f': '\\f',
205 | '\r': '\\r',
206 | '"' : '\\"',
207 | '\\': '\\\\'
208 | },
209 | rep;
210 |
211 |
212 | function quote(string) {
213 |
214 | // If the string contains no control characters, no quote characters, and no
215 | // backslash characters, then we can safely slap some quotes around it.
216 | // Otherwise we must also replace the offending characters with safe escape
217 | // sequences.
218 |
219 | escapable.lastIndex = 0;
220 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
221 | var c = meta[a];
222 | return typeof c === 'string'
223 | ? c
224 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
225 | }) + '"' : '"' + string + '"';
226 | }
227 |
228 |
229 | function str(key, holder) {
230 |
231 | // Produce a string from holder[key].
232 |
233 | var i, // The loop counter.
234 | k, // The member key.
235 | v, // The member value.
236 | length,
237 | mind = gap,
238 | partial,
239 | value = holder[key];
240 |
241 | // If the value has a toJSON method, call it to obtain a replacement value.
242 |
243 | if (value && typeof value === 'object' &&
244 | typeof value.toJSON === 'function') {
245 | value = value.toJSON(key);
246 | }
247 |
248 | // If we were called with a replacer function, then call the replacer to
249 | // obtain a replacement value.
250 |
251 | if (typeof rep === 'function') {
252 | value = rep.call(holder, key, value);
253 | }
254 |
255 | // What happens next depends on the value's type.
256 |
257 | switch (typeof value) {
258 | case 'string':
259 | return quote(value);
260 |
261 | case 'number':
262 |
263 | // JSON numbers must be finite. Encode non-finite numbers as null.
264 |
265 | return isFinite(value) ? String(value) : 'null';
266 |
267 | case 'boolean':
268 | case 'null':
269 |
270 | // If the value is a boolean or null, convert it to a string. Note:
271 | // typeof null does not produce 'null'. The case is included here in
272 | // the remote chance that this gets fixed someday.
273 |
274 | return String(value);
275 |
276 | // If the type is 'object', we might be dealing with an object or an array or
277 | // null.
278 |
279 | case 'object':
280 |
281 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
282 | // so watch out for that case.
283 |
284 | if (!value) {
285 | return 'null';
286 | }
287 |
288 | // Make an array to hold the partial results of stringifying this object value.
289 |
290 | gap += indent;
291 | partial = [];
292 |
293 | // Is the value an array?
294 |
295 | if (Object.prototype.toString.apply(value) === '[object Array]') {
296 |
297 | // The value is an array. Stringify every element. Use null as a placeholder
298 | // for non-JSON values.
299 |
300 | length = value.length;
301 | for (i = 0; i < length; i += 1) {
302 | partial[i] = str(i, value) || 'null';
303 | }
304 |
305 | // Join all of the elements together, separated with commas, and wrap them in
306 | // brackets.
307 |
308 | v = partial.length === 0
309 | ? '[]'
310 | : gap
311 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
312 | : '[' + partial.join(',') + ']';
313 | gap = mind;
314 | return v;
315 | }
316 |
317 | // If the replacer is an array, use it to select the members to be stringified.
318 |
319 | if (rep && typeof rep === 'object') {
320 | length = rep.length;
321 | for (i = 0; i < length; i += 1) {
322 | if (typeof rep[i] === 'string') {
323 | k = rep[i];
324 | v = str(k, value);
325 | if (v) {
326 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
327 | }
328 | }
329 | }
330 | } else {
331 |
332 | // Otherwise, iterate through all of the keys in the object.
333 |
334 | for (k in value) {
335 | if (Object.prototype.hasOwnProperty.call(value, k)) {
336 | v = str(k, value);
337 | if (v) {
338 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
339 | }
340 | }
341 | }
342 | }
343 |
344 | // Join all of the member texts together, separated with commas,
345 | // and wrap them in braces.
346 |
347 | v = partial.length === 0
348 | ? '{}'
349 | : gap
350 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
351 | : '{' + partial.join(',') + '}';
352 | gap = mind;
353 | return v;
354 | }
355 | }
356 |
357 | // If the JSON object does not yet have a stringify method, give it one.
358 |
359 | if (typeof JSON.stringify !== 'function') {
360 | JSON.stringify = function (value, replacer, space) {
361 |
362 | // The stringify method takes a value and an optional replacer, and an optional
363 | // space parameter, and returns a JSON text. The replacer can be a function
364 | // that can replace values, or an array of strings that will select the keys.
365 | // A default replacer method can be provided. Use of the space parameter can
366 | // produce text that is more easily readable.
367 |
368 | var i;
369 | gap = '';
370 | indent = '';
371 |
372 | // If the space parameter is a number, make an indent string containing that
373 | // many spaces.
374 |
375 | if (typeof space === 'number') {
376 | for (i = 0; i < space; i += 1) {
377 | indent += ' ';
378 | }
379 |
380 | // If the space parameter is a string, it will be used as the indent string.
381 |
382 | } else if (typeof space === 'string') {
383 | indent = space;
384 | }
385 |
386 | // If there is a replacer, it must be a function or an array.
387 | // Otherwise, throw an error.
388 |
389 | rep = replacer;
390 | if (replacer && typeof replacer !== 'function' &&
391 | (typeof replacer !== 'object' ||
392 | typeof replacer.length !== 'number')) {
393 | throw new Error('JSON.stringify');
394 | }
395 |
396 | // Make a fake root object containing our value under the key of ''.
397 | // Return the result of stringifying the value.
398 |
399 | return str('', {'': value});
400 | };
401 | }
402 |
403 |
404 | // If the JSON object does not yet have a parse method, give it one.
405 |
406 | if (typeof JSON.parse !== 'function') {
407 | JSON.parse = function (text, reviver) {
408 |
409 | // The parse method takes a text and an optional reviver function, and returns
410 | // a JavaScript value if the text is a valid JSON text.
411 |
412 | var j;
413 |
414 | function walk(holder, key) {
415 |
416 | // The walk method is used to recursively walk the resulting structure so
417 | // that modifications can be made.
418 |
419 | var k, v, value = holder[key];
420 | if (value && typeof value === 'object') {
421 | for (k in value) {
422 | if (Object.prototype.hasOwnProperty.call(value, k)) {
423 | v = walk(value, k);
424 | if (v !== undefined) {
425 | value[k] = v;
426 | } else {
427 | delete value[k];
428 | }
429 | }
430 | }
431 | }
432 | return reviver.call(holder, key, value);
433 | }
434 |
435 |
436 | // Parsing happens in four stages. In the first stage, we replace certain
437 | // Unicode characters with escape sequences. JavaScript handles many characters
438 | // incorrectly, either silently deleting them, or treating them as line endings.
439 |
440 | text = String(text);
441 | cx.lastIndex = 0;
442 | if (cx.test(text)) {
443 | text = text.replace(cx, function (a) {
444 | return '\\u' +
445 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
446 | });
447 | }
448 |
449 | // In the second stage, we run the text against regular expressions that look
450 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
451 | // because they can cause invocation, and '=' because it can cause mutation.
452 | // But just to be safe, we want to reject all unexpected forms.
453 |
454 | // We split the second stage into 4 regexp operations in order to work around
455 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
456 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
457 | // replace all simple value tokens with ']' characters. Third, we delete all
458 | // open brackets that follow a colon or comma or that begin the text. Finally,
459 | // we look to see that the remaining characters are only whitespace or ']' or
460 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
461 |
462 | if (/^[\],:{}\s]*$/
463 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
464 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
465 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
466 |
467 | // In the third stage we use the eval function to compile the text into a
468 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
469 | // in JavaScript: it can begin a block or an object literal. We wrap the text
470 | // in parens to eliminate the ambiguity.
471 |
472 | j = eval('(' + text + ')');
473 |
474 | // In the optional fourth stage, we recursively walk the new structure, passing
475 | // each name/value pair to a reviver function for possible transformation.
476 |
477 | return typeof reviver === 'function'
478 | ? walk({'': j}, '')
479 | : j;
480 | }
481 |
482 | // If the text is not JSON parseable, then a SyntaxError is thrown.
483 |
484 | throw new SyntaxError('JSON.parse');
485 | };
486 | }
487 | }());
488 |
--------------------------------------------------------------------------------
/assets/Goldprice/main.css:
--------------------------------------------------------------------------------
1 | #reply {
2 | font-size: 18px;
3 | color: #ff2600;
4 | font-weight: bold;
5 | }
--------------------------------------------------------------------------------
/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/ic_launcher-web.png
--------------------------------------------------------------------------------
/libs/cordova-1.5.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/libs/cordova-1.5.0.jar
--------------------------------------------------------------------------------
/libs/jackson-core-asl-1.9.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/libs/jackson-core-asl-1.9.7.jar
--------------------------------------------------------------------------------
/libs/jackson-mapper-asl-1.9.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/libs/jackson-mapper-asl-1.9.7.jar
--------------------------------------------------------------------------------
/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 | # Project target.
14 | target=android-10
15 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_action_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-hdpi/ic_action_search.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_action_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-mdpi/ic_action_search.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_action_search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-xhdpi/ic_action_search.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jollen/android-browser-websocket/011e7cf313f8f58afd032bc278ea7849617ca2db/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | com.moko365.phonegap
4 | Hello world!
5 | Settings
6 | MainActivity
7 |
8 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/res/xml/plugins.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/com/moko365/android/websocket/MainActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 Moko365 Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.moko365.android.websocket;
17 |
18 | import android.os.Bundle;
19 |
20 | public class MainActivity extends WebsocketDroidGap {
21 |
22 | @Override
23 | public void onCreate(Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | super.loadUrl("file:///android_asset/Goldprice/index.html");
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/com/moko365/android/websocket/WebSocketClientBinding.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 Moko365 Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.moko365.android.websocket;
17 |
18 | import de.tavendo.autobahn.WebSocketConnection;
19 | import de.tavendo.autobahn.WebSocketException;
20 | import android.util.Log;
21 |
22 | public class WebSocketClientBinding {
23 |
24 | private WebsocketDroidGap mContext;
25 | private WebSocketConnection mConnection;
26 | private WebsocketHandler mHandler;
27 |
28 | public WebSocketClientBinding(WebsocketDroidGap context) {
29 | Log.i(TAG, "WebSocketClientBinding()");
30 |
31 | mContext = context;
32 | }
33 |
34 | private static final String TAG = "WebSocketClientBinding";
35 |
36 | public void connect(String uri, String proto) {
37 | Log.i(TAG, "Protocol: " + proto);
38 | Log.i(TAG, "URL: " + uri);
39 |
40 | mConnection = new WebSocketConnection();
41 | mHandler = new WebsocketHandler(mContext);
42 |
43 | try {
44 | mConnection.connect(uri, proto, mHandler);
45 | } catch (WebSocketException e) {
46 | // TODO Auto-generated catch block
47 | e.printStackTrace();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/com/moko365/android/websocket/WebSocketHolder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 Moko365 Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.moko365.android.websocket;
17 |
18 | public interface WebSocketHolder {
19 |
20 | public void onConnectionError();
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/com/moko365/android/websocket/WebsocketDroidGap.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 Moko365 Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.moko365.android.websocket;
17 |
18 | import org.apache.cordova.DroidGap;
19 |
20 | import android.util.Log;
21 | import android.webkit.WebView;
22 |
23 | public class WebsocketDroidGap extends DroidGap {
24 |
25 | @Override
26 | public void init() {
27 | super.init();
28 | initWebsocket();
29 | }
30 |
31 | private static final String TAG = "WebsocketDropGap";
32 |
33 | @Override
34 | public void loadUrl(String url) {
35 | super.loadUrl(url);
36 | }
37 |
38 | private void initWebsocket() {
39 | Log.i(TAG, "initWebsocket");
40 |
41 | this.appView.getSettings().setJavaScriptEnabled(true);
42 | this.appView.addJavascriptInterface(new WebSocketClientBinding(this), "WebSocketAndroid");
43 | }
44 |
45 | public WebView getAppView() {
46 | return this.appView;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/com/moko365/android/websocket/WebsocketHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 Moko365 Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.moko365.android.websocket;
17 |
18 | import de.tavendo.autobahn.WebSocket;
19 | import de.tavendo.autobahn.WebSocketConnection;
20 | import de.tavendo.autobahn.WebSocketConnectionHandler;
21 |
22 | import android.content.Context;
23 | import android.os.Handler;
24 | import android.os.Message;
25 | import android.util.Log;
26 |
27 | public class WebsocketHandler extends WebSocketConnectionHandler implements WebSocketHolder {
28 |
29 | private static final String TAG = "WebsocketHandler";
30 |
31 | public static final int WEBSOCKET_ONOPEN = 0;
32 | public static final int WEBSOCKET_ONCLOSE = 1;
33 | public static final int WEBSOCKET_ONERROR = 2;
34 | public static final int WEBSOCKET_ONMESSAGE = 3;
35 |
36 | private WebsocketDroidGap mContext;
37 |
38 | public WebsocketHandler(WebsocketDroidGap context) {
39 | mContext = context;
40 | }
41 |
42 | @Override
43 | public void onOpen() {
44 | Log.i(TAG, "onOpen");
45 |
46 | mContext.getAppView().loadUrl("javascript: var ws = WebSocketImpl(); ws.onopen();");
47 | super.onOpen();
48 | }
49 |
50 | @Override
51 | public void onTextMessage(String payload) {
52 | Log.i(TAG, "onTextMessage");
53 |
54 | String jsObj = "{data: '" + payload + "'}";
55 | mContext.getAppView().loadUrl("javascript: var ws = WebSocketImpl(); ws.onmessage(" + jsObj + ");");
56 |
57 | super.onTextMessage(payload);
58 | }
59 |
60 | @Override
61 | public void onRawTextMessage(byte[] payload) {
62 | Log.i(TAG, "onRawTextMessage");
63 | super.onRawTextMessage(payload);
64 | }
65 |
66 | @Override
67 | public void onBinaryMessage(byte[] payload) {
68 | Log.i(TAG, "onBinaryMessage");
69 | super.onBinaryMessage(payload);
70 | }
71 |
72 |
73 | public void onConnectionError() {
74 | Log.i(TAG, "onConnectionError");
75 |
76 | mContext.getAppView().loadUrl("javascript: var ws = WebSocketImpl(); ws.onerror();");
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/de/.svn/all-wcprops:
--------------------------------------------------------------------------------
1 | K 25
2 | svn:wc:ra_dav:version-url
3 | V 34
4 | /svn/!svn/ver/16/trunk/src/main/de
5 | END
6 |
--------------------------------------------------------------------------------
/src/de/.svn/entries:
--------------------------------------------------------------------------------
1 | 10
2 |
3 | dir
4 | 17
5 | http://weberknecht.googlecode.com/svn/trunk/src/main/de
6 | http://weberknecht.googlecode.com/svn
7 |
8 |
9 |
10 | 2011-09-04T12:34:35.283792Z
11 | 16
12 | roderick.baier@gmail.com
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 0874249d-7fda-0166-25b4-43f8b00cd9cc
28 |
29 | roderick
30 | dir
31 |
32 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/ByteBufferInputStream.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.nio.ByteBuffer;
24 |
25 | /**
26 | * InputStream wrapping a ByteBuffer. This class can be used i.e. to wrap
27 | * ByteBuffers allocated direct in NIO for socket I/O. The class does not
28 | * allocate ByteBuffers itself, but assumes the user has already one that
29 | * just needs to be wrapped to use with InputStream based processing.
30 | */
31 | public class ByteBufferInputStream extends InputStream {
32 |
33 | /// ByteBuffer backing this input stream.
34 | private final ByteBuffer mBuffer;
35 |
36 | /**
37 | * Create input stream over ByteBuffer.
38 | *
39 | * @param buffer ByteBuffer to wrap as input stream.
40 | */
41 | public ByteBufferInputStream(ByteBuffer buffer) {
42 | mBuffer = buffer;
43 | }
44 |
45 | /**
46 | * Read one byte from input stream and advance.
47 | *
48 | * @return Byte read or -1 when stream end reached.
49 | */
50 | @Override
51 | public synchronized int read() throws IOException {
52 |
53 | if (!mBuffer.hasRemaining()) {
54 | return -1;
55 | } else {
56 | return mBuffer.get() & 0xFF;
57 | }
58 | }
59 |
60 | /**
61 | * Read chunk of bytes from input stream and advance. Read either as many
62 | * bytes specified or input stream end reached.
63 | *
64 | * @param bytes Read bytes into byte array.
65 | * @param off Read bytes into byte array beginning at this offset.
66 | * @param len Read at most this many bytes.
67 | * @return Actual number of bytes read.
68 | */
69 | @Override
70 | public synchronized int read(byte[] bytes, int off, int len)
71 | throws IOException {
72 |
73 | if (bytes == null) {
74 | throw new NullPointerException();
75 | } else if (off < 0 || len < 0 || len > bytes.length - off) {
76 | throw new IndexOutOfBoundsException();
77 | } else if (len == 0) {
78 | return 0;
79 | }
80 |
81 | int length = Math.min(mBuffer.remaining(), len);
82 | if (length == 0) {
83 | return -1;
84 | }
85 |
86 | mBuffer.get(bytes, off, length);
87 | return length;
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/ByteBufferOutputStream.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.IOException;
22 | import java.io.OutputStream;
23 | import java.nio.Buffer;
24 | import java.nio.ByteBuffer;
25 |
26 | /**
27 | * OutputStream wrapping a ByteBuffer. This class internally allocates a
28 | * direct ByteBuffer for use i.e. with NIO for socket I/O. The ByteBuffer
29 | * is automatically enlarged if needed (preserving contents when enlarged).
30 | */
31 | public class ByteBufferOutputStream extends OutputStream {
32 |
33 | /// Initial size of allocated ByteBuffer.
34 | private final int mInitialSize;
35 |
36 | /// Amount to grow when ByteBuffer needs to be enlarged.
37 | private final int mGrowSize;
38 |
39 | /// Internal ByteBuffer wrapped.
40 | private ByteBuffer mBuffer;
41 |
42 | /**
43 | * Create a direct allocated ByteBuffer wrapped as OutputStream.
44 | */
45 | public ByteBufferOutputStream() {
46 | this(2 * 65536, 65536);
47 | }
48 |
49 | /**
50 | * Create a direct allocated ByteBuffer wrapped as OutputStream.
51 | *
52 | * @param initialSize Initial size of ByteBuffer.
53 | * @param growSize When buffer needs to grow, enlarge by this amount.
54 | */
55 | public ByteBufferOutputStream(int initialSize, int growSize) {
56 | mInitialSize = initialSize;
57 | mGrowSize = growSize;
58 | mBuffer = ByteBuffer.allocateDirect(mInitialSize);
59 | mBuffer.clear();
60 | }
61 |
62 | /**
63 | * Get the underlying ByteBuffer.
64 | *
65 | * @return ByteBuffer underlying this OutputStream.
66 | */
67 | public ByteBuffer getBuffer() {
68 | return mBuffer;
69 | }
70 |
71 | /**
72 | * Calls flip on the underyling ByteBuffer.
73 | */
74 | public Buffer flip() {
75 | return mBuffer.flip();
76 | }
77 |
78 | /**
79 | * Calls clear on the underlying ByteBuffer.
80 | */
81 | public Buffer clear() {
82 | return mBuffer.clear();
83 | }
84 |
85 | /**
86 | * Calls remaining() on underlying ByteBuffer.
87 | */
88 | public int remaining() {
89 | return mBuffer.remaining();
90 | }
91 |
92 | /**
93 | * Expand the underlying ByteBuffer and preserve content.
94 | *
95 | * @param requestSize Requested new size.
96 | */
97 | public synchronized void expand(int requestSize) {
98 |
99 | if (requestSize > mBuffer.capacity()) {
100 |
101 | ByteBuffer oldBuffer = mBuffer;
102 | int oldPosition = mBuffer.position();
103 | int newCapacity = ((requestSize / mGrowSize) + 1) * mGrowSize;
104 | mBuffer = ByteBuffer.allocateDirect(newCapacity);
105 | oldBuffer.clear();
106 | mBuffer.clear();
107 | mBuffer.put(oldBuffer);
108 | mBuffer.position(oldPosition);
109 | }
110 | }
111 |
112 | /**
113 | * Write one byte to the underlying ByteBuffer via this OutputStream.
114 | *
115 | * @param b Byte to be written.
116 | */
117 | @Override
118 | public synchronized void write(int b) throws IOException {
119 |
120 | if (mBuffer.position() + 1 > mBuffer.capacity()) {
121 | expand(mBuffer.capacity() + 1);
122 | }
123 | mBuffer.put((byte) b);
124 | }
125 |
126 | /**
127 | * Write a chunk of bytes to the underyling ByteBuffer via this
128 | * OutputStream.
129 | *
130 | * @param bytes Write bytes from this byte array.
131 | * @param off Start reading at this offset within byte array.
132 | * @param len Write this many bytes, and enlarge underyling
133 | * ByteBuffer when necessary, preserving the contents.
134 | */
135 | @Override
136 | public synchronized void write(byte[] bytes, int off, int len)
137 | throws IOException {
138 |
139 | if (mBuffer.position() + len > mBuffer.capacity()) {
140 | expand(mBuffer.capacity() + len);
141 | }
142 | mBuffer.put(bytes, off, len);
143 | }
144 |
145 | /**
146 | * Write a complete byte array to the underlying ByteBuffer via this
147 | * OutputStream.
148 | *
149 | * @param bytes Byte array to be written.
150 | */
151 | public synchronized void write(byte[] bytes) throws IOException {
152 | write(bytes, 0, bytes.length);
153 | }
154 |
155 | /**
156 | * Write the UTF-8 encoding of a String to the underlying ByteBuffer
157 | * via this OutputStream.
158 | *
159 | * @param str String to be written.
160 | * @throws IOException
161 | */
162 | public synchronized void write(String str) throws IOException {
163 | write(str.getBytes("UTF-8"));
164 | }
165 |
166 | /**
167 | * Write CR-LF.
168 | *
169 | * @throws IOException
170 | */
171 | public synchronized void crlf() throws IOException {
172 | write(0x0d);
173 | write(0x0a);
174 | }
175 |
176 | }
177 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/Doxygen.java:
--------------------------------------------------------------------------------
1 | package de.tavendo.autobahn;
2 |
3 | /*!
4 | \mainpage
5 | \section intro_sec AutobahnAndroid API Reference
6 |
7 | AutobahnAndroid provides a Java client library implementing
8 | The WebSocket Protocol and
9 | The WebSocket Application Messaging Protocol for use
10 | in native Android apps.
11 |
12 |
13 | \section websocket_features WebSocket Support
14 |
15 | AutobahnAndroid implements the WebSocket protocol
16 | with a couple of distinct features:
17 |
18 | \li full RFC6455 and Draft Hybi-10 to -17 support
19 | \li very good standards conformance
20 | \li performant
21 | \li easy to use API
22 | \li designed to work with Android UI applications
23 | \li Open-Source, licensed under the Apache 2.0 license
24 |
25 | The implementation passes all (nearly 300) tests from the
26 | AutobahnTestSuite.
27 |
28 | The basic API is modeled after the WebSocket JavaScript API for
29 | ease of use and familarity.
30 |
31 | The API enables the use of common Android idioms for event handling (using
32 | anonymous inner classes) and integrates with Android UI applications (by
33 | communicating via messages and message loops between the UI thread and back-
34 | ground reader/writer threads and by avoiding _any_ network activity on the
35 | UI thread).
36 |
37 | The implementation uses Java NIO to reduce network processing overhead and
38 | is on-par or faster performance-wise compared to Firefox 8 Mobile, a C++
39 | implementation of WebSockets.
40 |
41 | \section rpc_pubsub WAMP (RPC/PubSub) Support
42 |
43 | AutobahnAndroid also
44 | includes an implementation of The WebSocket Application Messaging Protocol (WAMP)
45 | which can be used to build applications around Remote Procedure Call and
46 | Publish & Subscribe messaging patterns.
47 |
48 | It features:
49 |
50 | \li RPC and PubSub, fully asynchronous design
51 | \li built on JSON and WebSockets
52 | \li simple, efficient and open protocol
53 | \li automatic mapping to user-defined POJOs
54 | \li seamless integration in Android UI apps
55 | \li Open-Source, licensed under the Apache 2.0 license
56 |
57 | Call results and events which travel the wire as JSON payload are automatically
58 | converted and mapped to Java primitive types or user-defined POJOs (Plain-old Java Objects).
59 |
60 | The latter is a very convenient and powerful feature made possible by the use of
61 | Jackson, a high-performance JSON processor.
62 | This works even for container types, such as lists or maps over POJOs.
63 |
64 | For example, it is possible to issue a RPC and get a List as a result, where Person is
65 | a user-defined class.
66 |
67 | \section usage Usage
68 |
69 | The only dependency of
70 | AutobahnAndroid
71 | is Jackson.
72 | To use, all one needs to do is to include the built JARs into an Android
73 | project.
74 |
75 | \section more More Information
76 |
77 | For more information, please visit the project page,
78 | the forum or the
79 | code repository.
80 | */
81 |
82 | /// Empty class file to hold Doxygen documentation.
83 | abstract class Doxygen {
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/NoCopyByteArrayOutputStream.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.ByteArrayInputStream;
22 | import java.io.ByteArrayOutputStream;
23 | import java.io.InputStream;
24 |
25 | /**
26 | * OutputStream backed by a byte array. This class provides copyless access
27 | * to byte array backing the ByteArrayOutputStream
28 | */
29 | public class NoCopyByteArrayOutputStream extends ByteArrayOutputStream {
30 |
31 | /**
32 | * Create new OutputStream backed by byte array.
33 | */
34 | public NoCopyByteArrayOutputStream() {
35 | super();
36 | }
37 |
38 | /**
39 | * Create new OutputStream backed by byte array.
40 | *
41 | * @param size Initial size of underlying byte array.
42 | */
43 | public NoCopyByteArrayOutputStream(int size) {
44 | super(size);
45 | }
46 |
47 | /**
48 | * Wraps the underyling byte array into an InputStream.
49 | *
50 | * @return New InputStream wrapping byte buffer underlying this stream.
51 | */
52 | public InputStream getInputStream() {
53 | return new ByteArrayInputStream(buf, 0, count);
54 | }
55 |
56 | /**
57 | * Get byte array underlying this OutputStream. This
58 | * does not copy any data, but return reference to the
59 | * underlying byte array.
60 | *
61 | * @return Underlying byte array by reference.
62 | */
63 | public byte[] getByteArray() {
64 | return buf;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/PrefixMap.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.util.HashMap;
22 |
23 | /**
24 | * Mapping between CURIEs and URIs.
25 | * Provides a two-way mapping between CURIEs (Compact URI Expressions) and
26 | * full URIs.
27 | *
28 | * \see http://www.w3.org/TR/curie/
29 | *
30 | * \todo Prefixes MUST be NCNames (http://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-NCName)
31 | *
32 | * \todo Work in the details of http://www.w3.org/TR/curie/ (default prefixes, ..)
33 | */
34 | public class PrefixMap {
35 |
36 | private final HashMap mPrefixes = new HashMap();
37 | private final HashMap mUris = new HashMap();
38 |
39 | /**
40 | * Set mapping of prefix to URI.
41 | *
42 | * @param prefix Prefix to be mapped.
43 | * @param uri URI the prefix is to be mapped to.
44 | */
45 | public void set(String prefix, String uri) {
46 | mPrefixes.put(prefix, uri);
47 | mUris.put(uri, prefix);
48 | }
49 |
50 | /**
51 | * Returns the URI for the prefix or None if prefix has no mapped URI.
52 | *
53 | * @param prefix Prefix to look up.
54 | * @return Mapped URI for prefix or None.
55 | */
56 | public String get(String prefix) {
57 | return mPrefixes.get(prefix);
58 | }
59 |
60 | /**
61 | * Remove mapping of prefix to URI.
62 | *
63 | * @param prefix Prefix for which mapping should be removed.
64 | * @return The URI the prefix was mapped to (when removed),
65 | * or null when prefix is unmapped (so there wasn't
66 | * anything to remove).
67 | */
68 | public String remove(String prefix) {
69 | if (mPrefixes.containsKey(prefix)) {
70 | String uri = mPrefixes.get(prefix);
71 | mPrefixes.remove(prefix);
72 | mUris.remove(uri);
73 | return uri;
74 | } else {
75 | return null;
76 | }
77 | }
78 |
79 | /**
80 | * Remove all prefix mappings.
81 | */
82 | public void clear() {
83 | mPrefixes.clear();
84 | mUris.clear();
85 | }
86 |
87 | /**
88 | * Resolve given CURIE to full URI.
89 | *
90 | * @param curie CURIE (i.e. "rdf:label").
91 | * @return Full URI for CURIE or None.
92 | */
93 | public String resolve(String curie) {
94 | int i = curie.indexOf(':');
95 | if (i > 0) {
96 | String prefix = curie.substring(0, i);
97 | if (mPrefixes.containsKey(prefix)) {
98 | return mPrefixes.get(prefix) + curie.substring(i + 1);
99 | }
100 | }
101 | return null;
102 | }
103 |
104 | /**
105 | * Resolve given CURIE/URI and return string verbatim if cannot be resolved.
106 | *
107 | * @param curieOrUri CURIE or URI.
108 | * @return Full URI for CURIE or original string.
109 | */
110 | public String resolveOrPass(String curieOrUri) {
111 |
112 | String u = resolve(curieOrUri);
113 | if (u != null) {
114 | return u;
115 | } else {
116 | return curieOrUri;
117 | }
118 | }
119 |
120 | /**
121 | * Shrink given URI to CURIE. If no appropriate prefix mapping is available,
122 | * return original URI.
123 | *
124 | * @param uri URI to shrink.
125 | * @return CURIE or original URI.
126 | */
127 | public String shrink(String uri) {
128 |
129 | for (int i = uri.length(); i > 0; --i) {
130 | String u = uri.substring(0, i);
131 | String p = mUris.get(u);
132 | if (p != null) {
133 | return p + ':' + uri.substring(i);
134 | }
135 | }
136 | return uri;
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/Utf8Validator.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | * Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
18 | * Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
19 | *
20 | ******************************************************************************/
21 |
22 | package de.tavendo.autobahn;
23 |
24 |
25 | /**
26 | * Incremental UTF-8 validator. The validator runs with constant memory
27 | * consumption (minimal state). Purpose is to validate UTF-8, not to
28 | * decode (which could be done easily also, but we rely on Java built in
29 | * facilities for that).
30 | *
31 | * Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
32 | * Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
33 | */
34 | public class Utf8Validator {
35 |
36 | /// DFA state transitions (14 x 32 = 448).
37 | private static final int[] DFA = {
38 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
39 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
40 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
41 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
42 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
43 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
44 | 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
45 | 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
46 | 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
47 | 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
48 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
49 | 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
50 | 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
51 | 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1 // s7..s8
52 | };
53 |
54 | private static final int ACCEPT = 0;
55 | private static final int REJECT = 1;
56 |
57 | private int mState;
58 | private int mPos;
59 |
60 | /**
61 | * Create new incremental UTF-8 validator. The validator is already
62 | * resetted and thus immediately usable.
63 | */
64 | public Utf8Validator() {
65 | reset();
66 | }
67 |
68 | /**
69 | * Reset validator state to begin validation of new
70 | * UTF-8 stream.
71 | */
72 | public void reset() {
73 | mState = ACCEPT;
74 | mPos = 0;
75 | }
76 |
77 | /**
78 | * Get end of validated position within stream. When validate()
79 | * returns false, indicating an UTF-8 error, this function can
80 | * be used to get the exact position within the stream upon
81 | * which the violation was encountered.
82 | *
83 | * @return Current position with stream validated.
84 | */
85 | public int position() {
86 | return mPos;
87 | }
88 |
89 | /**
90 | * Check if incremental validation (currently) has ended on
91 | * a complete encoded Unicode codepoint.
92 | *
93 | * @return True, iff currently ended on codepoint.
94 | */
95 | public boolean isValid() {
96 | return mState == ACCEPT;
97 | }
98 |
99 | /**
100 | * Validate a chunk of octets for UTF-8.
101 | *
102 | * @param data Buffer which contains chunk to validate.
103 | * @param off Offset within buffer where to continue with validation.
104 | * @param len Length in octets to validate within buffer.
105 | * @return False as soon as UTF-8 violation occurs, true otherwise.
106 | */
107 | public boolean validate(byte[] data, int off, int len) {
108 | for (int i = off; i < off + len; ++i) {
109 | mState = DFA[256 + (mState << 4) + DFA[(int) (0xff & data[i])]];
110 | if (mState == REJECT) {
111 | mPos += i;
112 | return false;
113 | }
114 | }
115 | mPos += len;
116 | return true;
117 | }
118 |
119 | /**
120 | * Validate a chunk of octets for UTF-8.
121 | *
122 | * @param data Buffer which contains chunk to validate.
123 | * @return False as soon as UTF-8 violation occurs, true otherwise.
124 | */
125 | public boolean validate(byte[] data) {
126 | return validate(data, 0, data.length);
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/Wamp.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 |
22 | import org.codehaus.jackson.type.TypeReference;
23 |
24 | /**
25 | * WAMP interface.
26 | */
27 | public interface Wamp {
28 |
29 | /**
30 | * Session handler for WAMP sessions.
31 | */
32 | public interface ConnectionHandler {
33 |
34 | /**
35 | * Fired upon successful establishment of connection to WAMP server.
36 | */
37 | public void onOpen();
38 |
39 | /**
40 | * Firex upon unsuccessful connection attempt or when connection
41 | * was closed normally, or abnormally.
42 | *
43 | * @param code The close code, which provides information about why the connection was closed.
44 | * @param reason A humand readable description of the reason of connection close.
45 | */
46 | public void onClose(int code, String reason);
47 | }
48 |
49 | /**
50 | * Connect to WAMP server.
51 | *
52 | * @param wsUri The WebSockets URI of the server.
53 | * @param sessionHandler The handler for the session.
54 | */
55 | public void connect(String wsUri, ConnectionHandler sessionHandler);
56 |
57 |
58 | /**
59 | * Connect to WAMP server.
60 | *
61 | * @param wsUri The WebSockets URI of the server.
62 | * @param sessionHandler The handler for the session.
63 | * @param options WebSockets and Autobahn option.s
64 | */
65 | public void connect(String wsUri, ConnectionHandler sessionHandler, WampOptions options);
66 |
67 |
68 | /**
69 | * Disconnect from WAMP server.
70 | */
71 | public void disconnect();
72 |
73 | /**
74 | * Check if currently connected to server.
75 | *
76 | * @return True, iff connected.
77 | */
78 | public boolean isConnected();
79 |
80 |
81 | /**
82 | * Establish a prefix to be used in CURIEs to shorten URIs.
83 | *
84 | * @param prefix The prefix to be used in CURIEs.
85 | * @param uri The full URI this prefix shall resolve to.
86 | */
87 | public void prefix(String prefix, String uri);
88 |
89 | /**
90 | * Call handler.
91 | */
92 | public interface CallHandler {
93 |
94 | /**
95 | * Fired on successful completion of call.
96 | *
97 | * @param result The RPC result transformed into the type that was specified in call.
98 | */
99 | public void onResult(Object result);
100 |
101 | /**
102 | * Fired on call failure.
103 | *
104 | * @param errorUri The URI or CURIE of the error that occurred.
105 | * @param errorDesc A human readable description of the error.
106 | */
107 | public void onError(String errorUri, String errorDesc);
108 | }
109 |
110 | /**
111 | * Call a remote procedure (RPC).
112 | *
113 | * @param procUri The URI or CURIE of the remote procedure to call.
114 | * @param resultType The type the call result gets transformed into.
115 | * @param callHandler The handler to be invoked upon call completion.
116 | * @param arguments Zero, one or more arguments for the call.
117 | */
118 | public void call(String procUri, Class> resultType, CallHandler callHandler, Object... arguments);
119 |
120 | /**
121 | * Call a remote procedure (RPC).
122 | *
123 | * @param procUri The URI or CURIE of the remote procedure to call.
124 | * @param resultType The type the call result gets transformed into.
125 | * @param callHandler The handler to be invoked upon call completion.
126 | * @param arguments Zero, one or more arguments for the call.
127 | */
128 | public void call(String procUri, TypeReference> resultType, CallHandler callHandler, Object... arguments);
129 |
130 | /**
131 | * Handler for PubSub events.
132 | */
133 | public interface EventHandler {
134 |
135 | /**
136 | * Fired when an event for the PubSub subscription is received.
137 | *
138 | * @param topicUri The URI or CURIE of the topic the event was published to.
139 | * @param event The event, transformed into the type that was specified when subscribing.
140 | */
141 | public void onEvent(String topicUri, Object event);
142 | }
143 |
144 | /**
145 | * Subscribe to a topic. When already subscribed, overwrite the event handler.
146 | *
147 | * @param topicUri The URI or CURIE of the topic to subscribe to.
148 | * @param eventType The type that event get transformed into.
149 | * @param eventHandler The event handler.
150 | */
151 | public void subscribe(String topicUri, Class> eventType, EventHandler eventHandler);
152 |
153 | /**
154 | * Subscribe to a topic. When already subscribed, overwrite the event handler.
155 | *
156 | * @param topicUri The URI or CURIE of the topic to subscribe to.
157 | * @param eventType The type that event get transformed into.
158 | * @param eventHandler The event handler.
159 | */
160 | public void subscribe(String topicUri, TypeReference> eventType, EventHandler eventHandler);
161 |
162 | /**
163 | * Unsubscribe from given topic.
164 | *
165 | * @param topicUri The URI or CURIE of the topic to unsubscribe from.
166 | */
167 | public void unsubscribe(String topicUri);
168 |
169 | /**
170 | * Unsubscribe from any topics subscribed.
171 | */
172 | public void unsubscribe();
173 |
174 | /**
175 | * Publish an event to the specified topic.
176 | *
177 | * @param topicUri The URI or CURIE of the topic the event is to be published for.
178 | * @param event The event to be published.
179 | */
180 | public void publish(String topicUri, Object event);
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WampConnection.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.util.Random;
22 | import java.util.concurrent.ConcurrentHashMap;
23 |
24 | import org.codehaus.jackson.type.TypeReference;
25 |
26 | import android.os.HandlerThread;
27 | import android.util.Log;
28 |
29 | public class WampConnection extends WebSocketConnection implements Wamp {
30 |
31 | private static final boolean DEBUG = true;
32 | private static final String TAG = WampConnection.class.getName();
33 |
34 |
35 | /// The message handler of the background writer.
36 | protected WampWriter mWriterHandler;
37 |
38 | /// Prefix map for outgoing messages.
39 | private final PrefixMap mOutgoingPrefixes = new PrefixMap();
40 |
41 | /// RNG for IDs.
42 | private final Random mRng = new Random();
43 |
44 | /// Set of chars to be used for IDs.
45 | private static final char[] mBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
46 | .toCharArray();
47 |
48 | /**
49 | * RPC metadata.
50 | */
51 | public static class CallMeta {
52 |
53 | CallMeta(CallHandler handler, Class> resultClass) {
54 | this.mResultHandler = handler;
55 | this.mResultClass = resultClass;
56 | this.mResultTypeRef = null;
57 | }
58 |
59 | CallMeta(CallHandler handler, TypeReference> resultTypeReference) {
60 | this.mResultHandler = handler;
61 | this.mResultClass = null;
62 | this.mResultTypeRef = resultTypeReference;
63 | }
64 |
65 | /// Call handler to be fired on.
66 | public CallHandler mResultHandler;
67 |
68 | /// Desired call result type or null.
69 | public Class> mResultClass;
70 |
71 | /// Desired call result type or null.
72 | public TypeReference> mResultTypeRef;
73 | }
74 |
75 | /// Metadata about issued, but not yet returned RPCs.
76 | private final ConcurrentHashMap mCalls = new ConcurrentHashMap();
77 |
78 | /**
79 | * Event subscription metadata.
80 | */
81 | public static class SubMeta {
82 |
83 | SubMeta(EventHandler handler, Class> resultClass) {
84 | this.mEventHandler = handler;
85 | this.mEventClass = resultClass;
86 | this.mEventTypeRef = null;
87 | }
88 |
89 | SubMeta(EventHandler handler, TypeReference> resultTypeReference) {
90 | this.mEventHandler = handler;
91 | this.mEventClass = null;
92 | this.mEventTypeRef = resultTypeReference;
93 | }
94 |
95 | /// Event handler to be fired on.
96 | public EventHandler mEventHandler;
97 |
98 | /// Desired event type or null.
99 | public Class> mEventClass;
100 |
101 | /// Desired event type or null.
102 | public TypeReference> mEventTypeRef;
103 | }
104 |
105 | /// Metadata about active event subscriptions.
106 | private final ConcurrentHashMap mSubs = new ConcurrentHashMap();
107 |
108 | /// The session handler provided to connect().
109 | private Wamp.ConnectionHandler mSessionHandler;
110 |
111 |
112 | /**
113 | * Create the connection transmitting leg writer.
114 | */
115 | protected void createWriter() {
116 |
117 | mWriterThread = new HandlerThread("AutobahnWriter");
118 | mWriterThread.start();
119 | mWriter = new WampWriter(mWriterThread.getLooper(), mMasterHandler, mTransportChannel, mOptions);
120 |
121 | if (DEBUG) Log.d(TAG, "writer created and started");
122 | }
123 |
124 |
125 | /**
126 | * Create the connection receiving leg reader.
127 | */
128 | protected void createReader() {
129 | mReader = new WampReader(mCalls, mSubs, mMasterHandler, mTransportChannel, mOptions, "AutobahnReader");
130 | mReader.start();
131 |
132 | if (DEBUG) Log.d(TAG, "reader created and started");
133 | }
134 |
135 |
136 | /**
137 | * Create new random ID. This is used, i.e. for use in RPC calls to correlate
138 | * call message with result message.
139 | *
140 | * @param len Length of ID.
141 | * @return New random ID of given length.
142 | */
143 | private String newId(int len) {
144 | char[] buffer = new char[len];
145 | for (int i = 0; i < len; i++) {
146 | buffer[i] = mBase64Chars[mRng.nextInt(mBase64Chars.length)];
147 | }
148 | return new String(buffer);
149 | }
150 |
151 |
152 | /**
153 | * Create new random ID of default length.
154 | *
155 | * @return New random ID of default length.
156 | */
157 | private String newId() {
158 | return newId(8);
159 | }
160 |
161 |
162 | public void connect(String wsUri, Wamp.ConnectionHandler sessionHandler) {
163 |
164 | WampOptions options = new WampOptions();
165 | options.setReceiveTextMessagesRaw(true);
166 | options.setMaxMessagePayloadSize(64*1024);
167 | options.setMaxFramePayloadSize(64*1024);
168 | options.setTcpNoDelay(true);
169 |
170 | connect(wsUri, sessionHandler, options);
171 | }
172 |
173 |
174 | /**
175 | * Connect to server.
176 | *
177 | * @param wsUri WebSockets server URI.
178 | * @param sessionHandler The session handler to fire callbacks on.
179 | */
180 | public void connect(String wsUri, Wamp.ConnectionHandler sessionHandler, WampOptions options) {
181 |
182 | mSessionHandler = sessionHandler;
183 |
184 | mCalls.clear();
185 | mSubs.clear();
186 | mOutgoingPrefixes.clear();
187 |
188 | try {
189 | connect(wsUri, "wamp", new WebSocketConnectionHandler() {
190 |
191 | @Override
192 | public void onOpen() {
193 | if (mSessionHandler != null) {
194 | mSessionHandler.onOpen();
195 | } else {
196 | if (DEBUG) Log.d(TAG, "could not call onOpen() .. handler already NULL");
197 | }
198 | }
199 |
200 | @Override
201 | public void onClose(int code, String reason) {
202 | if (mSessionHandler != null) {
203 | mSessionHandler.onClose(code, reason);
204 | } else {
205 | if (DEBUG) Log.d(TAG, "could not call onClose() .. handler already NULL");
206 | }
207 | }
208 |
209 | }, options);
210 |
211 | } catch (WebSocketException e) {
212 |
213 | if (mSessionHandler != null) {
214 | mSessionHandler.onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT, "cannot connect (" + e.toString() + ")");
215 | } else {
216 | if (DEBUG) Log.d(TAG, "could not call onClose() .. handler already NULL");
217 | }
218 | }
219 |
220 | }
221 |
222 |
223 | /**
224 | * Process WAMP messages coming from the background reader.
225 | */
226 | protected void processAppMessage(Object message) {
227 |
228 | if (message instanceof WampMessage.CallResult) {
229 |
230 | WampMessage.CallResult callresult = (WampMessage.CallResult) message;
231 |
232 | if (mCalls.containsKey(callresult.mCallId)) {
233 | CallMeta meta = mCalls.get(callresult.mCallId);
234 | if (meta.mResultHandler != null) {
235 | meta.mResultHandler.onResult(callresult.mResult);
236 | }
237 | mCalls.remove(callresult.mCallId);
238 | }
239 |
240 | } else if (message instanceof WampMessage.CallError) {
241 |
242 | WampMessage.CallError callerror = (WampMessage.CallError) message;
243 |
244 | if (mCalls.containsKey(callerror.mCallId)) {
245 | CallMeta meta = mCalls.get(callerror.mCallId);
246 | if (meta.mResultHandler != null) {
247 | meta.mResultHandler.onError(callerror.mErrorUri, callerror.mErrorDesc);
248 | }
249 | mCalls.remove(callerror.mCallId);
250 | }
251 | } else if (message instanceof WampMessage.Event) {
252 |
253 | WampMessage.Event event = (WampMessage.Event) message;
254 |
255 | if (mSubs.containsKey(event.mTopicUri)) {
256 | SubMeta meta = mSubs.get(event.mTopicUri);
257 | if (meta != null && meta.mEventHandler != null) {
258 | meta.mEventHandler.onEvent(event.mTopicUri, event.mEvent);
259 | }
260 | }
261 | } else if (message instanceof WampMessage.Welcome) {
262 |
263 | WampMessage.Welcome welcome = (WampMessage.Welcome) message;
264 |
265 | // FIXME: safe session ID / fire session opened hook
266 | if (DEBUG) Log.d(TAG, "WAMP session " + welcome.mSessionId + " established (protocol version " + welcome.mProtocolVersion + ", server " + welcome.mServerIdent + ")");
267 |
268 | } else {
269 |
270 | if (DEBUG) Log.d(TAG, "unknown WAMP message in AutobahnConnection.processAppMessage");
271 | }
272 | }
273 |
274 |
275 | /**
276 | * Issue a remote procedure call (RPC).
277 | *
278 | * @param procUri URI or CURIE of procedure to call.
279 | * @param resultMeta Call result metadata.
280 | * @param arguments Call arguments.
281 | */
282 | private void call(String procUri, CallMeta resultMeta, Object... arguments) {
283 |
284 | WampMessage.Call call = new WampMessage.Call(newId(), procUri, arguments.length);
285 | for (int i = 0; i < arguments.length; ++i) {
286 | call.mArgs[i] = arguments[i];
287 | }
288 | mWriter.forward(call);
289 | mCalls.put(call.mCallId, resultMeta);
290 | }
291 |
292 |
293 | /**
294 | * Issue a remote procedure call (RPC). This version should be used with
295 | * primitive Java types and simple composite (class) types.
296 | *
297 | * @param procUri URI or CURIE of procedure to call.
298 | * @param resultType Type we want the call result to be converted to.
299 | * @param resultHandler Call handler to process call result or error.
300 | * @param arguments Call arguments.
301 | */
302 | public void call(String procUri, Class> resultType, CallHandler resultHandler, Object... arguments) {
303 |
304 | call(procUri, new CallMeta(resultHandler, resultType), arguments);
305 | }
306 |
307 |
308 | /**
309 | * Issue a remote procedure call (RPC). This version should be used with
310 | * result types which are containers, i.e. List<> or Map<>.
311 | *
312 | * @param procUri URI or CURIE of procedure to call.
313 | * @param resultType Type we want the call result to be converted to.
314 | * @param resultHandler Call handler to process call result or error.
315 | * @param arguments Call arguments.
316 | */
317 | public void call(String procUri, TypeReference> resultType, CallHandler resultHandler, Object... arguments) {
318 |
319 | call(procUri, new CallMeta(resultHandler, resultType), arguments);
320 | }
321 |
322 |
323 | /**
324 | * Subscribe to topic to receive events for.
325 | *
326 | * @param topicUri URI or CURIE of topic to subscribe to.
327 | * @param meta Subscription metadata.
328 | */
329 | private void subscribe(String topicUri, SubMeta meta) {
330 |
331 | String uri = mOutgoingPrefixes.resolveOrPass(topicUri);
332 |
333 | if (!mSubs.containsKey(uri)) {
334 |
335 | WampMessage.Subscribe msg = new WampMessage.Subscribe(mOutgoingPrefixes.shrink(topicUri));
336 | mWriter.forward(msg);
337 | }
338 | mSubs.put(uri, meta);
339 | }
340 |
341 |
342 | /**
343 | * Subscribe to topic to receive events for. This version should be used with
344 | * result types which are containers, i.e. List<> or Map<>.
345 | *
346 | * @param topicUri URI or CURIE of topic to subscribe to.
347 | * @param eventType The type we want events to be converted to.
348 | * @param eventHandler The event handler to process received events.
349 | */
350 | public void subscribe(String topicUri, Class> eventType, EventHandler eventHandler) {
351 |
352 | subscribe(topicUri, new SubMeta(eventHandler, eventType));
353 | }
354 |
355 |
356 | /**
357 | * Subscribe to topic to receive events for. This version should be used with
358 | * primitive Java types and simple composite (class) types.
359 | *
360 | * @param topicUri URI or CURIE of topic to subscribe to.
361 | * @param eventType The type we want events to be converted to.
362 | * @param eventHandler The event handler to process received events.
363 | */
364 | public void subscribe(String topicUri, TypeReference> eventType, EventHandler eventHandler) {
365 |
366 | subscribe(topicUri, new SubMeta(eventHandler, eventType));
367 | }
368 |
369 |
370 | /**
371 | * Unsubscribe from topic.
372 | *
373 | * @param topicUri URI or CURIE of topic to unsubscribe from.
374 | */
375 | public void unsubscribe(String topicUri) {
376 |
377 | if (mSubs.containsKey(topicUri)) {
378 |
379 | WampMessage.Unsubscribe msg = new WampMessage.Unsubscribe(topicUri);
380 | mWriter.forward(msg);
381 | }
382 | }
383 |
384 |
385 | /**
386 | * Unsubscribe from any subscribed topic.
387 | */
388 | public void unsubscribe() {
389 |
390 | for (String topicUri : mSubs.keySet()) {
391 |
392 | WampMessage.Unsubscribe msg = new WampMessage.Unsubscribe(topicUri);
393 | mWriter.forward(msg);
394 | }
395 | }
396 |
397 |
398 | /**
399 | * Establish a prefix to be used in CURIEs.
400 | *
401 | * @param prefix The prefix to be used in CURIEs.
402 | * @param uri The full URI this prefix shall resolve to.
403 | */
404 | public void prefix(String prefix, String uri) {
405 |
406 | String currUri = mOutgoingPrefixes.get(prefix);
407 |
408 | if (currUri == null || !currUri.equals(uri)) {
409 |
410 | mOutgoingPrefixes.set(prefix, uri);
411 |
412 | WampMessage.Prefix msg = new WampMessage.Prefix(prefix, uri);
413 | mWriter.forward(msg);
414 | }
415 | }
416 |
417 |
418 | /**
419 | * Publish an event to a topic.
420 | *
421 | * @param topicUri URI or CURIE of topic to publish event on.
422 | * @param event Event to be published.
423 | */
424 | public void publish(String topicUri, Object event) {
425 |
426 | WampMessage.Publish msg = new WampMessage.Publish(mOutgoingPrefixes.shrink(topicUri), event);
427 | mWriter.forward(msg);
428 | }
429 | }
430 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WampConnectionHandler.java:
--------------------------------------------------------------------------------
1 | package de.tavendo.autobahn;
2 |
3 | public class WampConnectionHandler implements Wamp.ConnectionHandler {
4 |
5 | public void onOpen() {
6 | // TODO Auto-generated method stub
7 |
8 | }
9 |
10 | public void onClose(int code, String reason) {
11 | // TODO Auto-generated method stub
12 |
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WampMessage.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 |
22 | /**
23 | * The master thread and the background reader/writer threads communicate
24 | * using these messages for Autobahn WAMP connections.
25 | */
26 | public class WampMessage {
27 |
28 | public static final int MESSAGE_TYPE_WELCOME = 0;
29 | public static final int MESSAGE_TYPE_PREFIX = 1;
30 | public static final int MESSAGE_TYPE_CALL = 2;
31 | public static final int MESSAGE_TYPE_CALL_RESULT = 3;
32 | public static final int MESSAGE_TYPE_CALL_ERROR = 4;
33 | public static final int MESSAGE_TYPE_SUBSCRIBE = 5;
34 | public static final int MESSAGE_TYPE_UNSUBSCRIBE = 6;
35 | public static final int MESSAGE_TYPE_PUBLISH = 7;
36 | public static final int MESSAGE_TYPE_EVENT = 8;
37 |
38 |
39 | /// Base message class.
40 | public static class Message extends WebSocketMessage.Message {
41 |
42 | }
43 |
44 | /**
45 | * RPC request message.
46 | * Client-to-server message.
47 | */
48 | public static class Call extends Message {
49 | public String mCallId;
50 | public String mProcUri;
51 | public Object[] mArgs;
52 |
53 | public Call(String callId, String procUri, int argCount) {
54 | mCallId = callId;
55 | mProcUri = procUri;
56 | mArgs = new Object[argCount];
57 | }
58 | }
59 |
60 | /**
61 | * RPC success response message.
62 | * Server-to-client message.
63 | */
64 | public static class CallResult extends Message {
65 | public String mCallId;
66 | public Object mResult;
67 |
68 | public CallResult(String callId, Object result) {
69 | mCallId = callId;
70 | mResult = result;
71 | }
72 | }
73 |
74 | /**
75 | * RPC failure response message.
76 | * Server-to-client message.
77 | */
78 | public static class CallError extends Message {
79 | public String mCallId;
80 | public String mErrorUri;
81 | public String mErrorDesc;
82 |
83 | public CallError(String callId, String errorUri, String errorDesc) {
84 | mCallId = callId;
85 | mErrorUri = errorUri;
86 | mErrorDesc = errorDesc;
87 | }
88 | }
89 |
90 | /**
91 | * Define Welcome message.
92 | * Server-to-client message.
93 | */
94 | public static class Welcome extends Message {
95 | public String mSessionId;
96 | public int mProtocolVersion;
97 | public String mServerIdent;
98 |
99 | public Welcome(String sessionId, int protocolVersion, String serverIdent) {
100 | mSessionId = sessionId;
101 | mProtocolVersion = protocolVersion;
102 | mServerIdent = serverIdent;
103 | }
104 | }
105 |
106 | /**
107 | * Define CURIE message.
108 | * Server-to-client and client-to-server message.
109 | */
110 | public static class Prefix extends Message {
111 | public String mPrefix;
112 | public String mUri;
113 |
114 | public Prefix(String prefix, String uri) {
115 | mPrefix = prefix;
116 | mUri = uri;
117 | }
118 | }
119 |
120 | /**
121 | * Publish to topic URI request message.
122 | * Client-to-server message.
123 | */
124 | public static class Publish extends Message {
125 | public String mTopicUri;
126 | public Object mEvent;
127 |
128 | public Publish(String topicUri, Object event) {
129 | mTopicUri = topicUri;
130 | mEvent = event;
131 | }
132 | }
133 |
134 | /**
135 | * Subscribe to topic URI request message.
136 | * Client-to-server message.
137 | */
138 | public static class Subscribe extends Message {
139 | public String mTopicUri;
140 |
141 | public Subscribe(String topicUri) {
142 | mTopicUri = topicUri;
143 | }
144 | }
145 |
146 | /**
147 | * Unsubscribe from topic URI request message.
148 | * Client-to-server message.
149 | */
150 | public static class Unsubscribe extends Message {
151 | public String mTopicUri;
152 |
153 | public Unsubscribe(String topicUri) {
154 | mTopicUri = topicUri;
155 | }
156 | }
157 |
158 | /**
159 | * Event on topic URI message.
160 | * Server-to-client message.
161 | */
162 | public static class Event extends Message {
163 | public String mTopicUri;
164 | public Object mEvent;
165 |
166 | public Event(String topicUri, Object event) {
167 | mTopicUri = topicUri;
168 | mEvent = event;
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WampOptions.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | public class WampOptions extends WebSocketOptions {
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WampReader.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.IOException;
22 | import java.nio.channels.SocketChannel;
23 | import java.util.concurrent.ConcurrentHashMap;
24 |
25 | import org.codehaus.jackson.JsonFactory;
26 | import org.codehaus.jackson.JsonParseException;
27 | import org.codehaus.jackson.JsonParser;
28 | import org.codehaus.jackson.JsonToken;
29 | import org.codehaus.jackson.map.DeserializationConfig;
30 | import org.codehaus.jackson.map.ObjectMapper;
31 |
32 | import android.os.Handler;
33 | import android.util.Log;
34 | import de.tavendo.autobahn.WampConnection.CallMeta;
35 | import de.tavendo.autobahn.WampConnection.SubMeta;
36 |
37 | /**
38 | * Autobahn WAMP reader, the receiving leg of a WAMP connection.
39 | */
40 | public class WampReader extends WebSocketReader {
41 |
42 | private static final boolean DEBUG = true;
43 | private static final String TAG = WampReader.class.getName();
44 |
45 | /// Jackson JSON-to-object mapper.
46 | private final ObjectMapper mJsonMapper;
47 |
48 | /// Jackson JSON factory from which we create JSON parsers.
49 | private final JsonFactory mJsonFactory;
50 |
51 | /// Holds reference to call map created on master.
52 | private final ConcurrentHashMap mCalls;
53 |
54 | /// Holds reference to event subscription map created on master.
55 | private final ConcurrentHashMap mSubs;
56 |
57 | /**
58 | * A reader object is created in AutobahnConnection.
59 | *
60 | * @param calls The call map created on master.
61 | * @param subs The event subscription map created on master.
62 | * @param master Message handler of master (used by us to notify the master).
63 | * @param socket The TCP socket.
64 | * @param options WebSockets connection options.
65 | * @param threadName The thread name we announce.
66 | */
67 | public WampReader(ConcurrentHashMap calls,
68 | ConcurrentHashMap subs,
69 | Handler master,
70 | SocketChannel socket,
71 | WebSocketOptions options,
72 | String threadName) {
73 |
74 | super(master, socket, options, threadName);
75 |
76 | mCalls = calls;
77 | mSubs = subs;
78 |
79 | mJsonMapper = new ObjectMapper();
80 | mJsonMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
81 | mJsonFactory = mJsonMapper.getJsonFactory();
82 |
83 | if (DEBUG) Log.d(TAG, "created");
84 | }
85 |
86 | protected void onTextMessage(String payload) {
87 |
88 | /// \todo make error propagation consistent
89 | notify(new WebSocketMessage.Error(new WebSocketException("non-raw receive of text message")));
90 | }
91 |
92 | protected void onBinaryMessage(byte[] payload) {
93 |
94 | /// \todo make error propagation consistent
95 | notify(new WebSocketMessage.Error(new WebSocketException("received binary message")));
96 | }
97 |
98 | /**
99 | * Unwraps a WAMP message which is a WebSockets text message with JSON
100 | * payload conforming to WAMP.
101 | */
102 | protected void onRawTextMessage(byte[] payload) {
103 |
104 | try {
105 |
106 | // create parser on top of raw UTF-8 payload
107 | JsonParser parser = mJsonFactory.createJsonParser(payload);
108 |
109 | // all Autobahn messages are JSON arrays
110 | if (parser.nextToken() == JsonToken.START_ARRAY) {
111 |
112 | // message type
113 | if (parser.nextToken() == JsonToken.VALUE_NUMBER_INT) {
114 |
115 | int msgType = parser.getIntValue();
116 |
117 | if (msgType == WampMessage.MESSAGE_TYPE_CALL_RESULT) {
118 |
119 | // call ID
120 | parser.nextToken();
121 | String callId = parser.getText();
122 |
123 | // result
124 | parser.nextToken();
125 | Object result = null;
126 |
127 | if (mCalls.containsKey(callId)) {
128 |
129 | CallMeta meta = mCalls.get(callId);
130 | if (meta.mResultClass != null) {
131 | result = parser.readValueAs(meta.mResultClass);
132 | } else if (meta.mResultTypeRef != null) {
133 | result = parser.readValueAs(meta.mResultTypeRef);
134 | } else {
135 | }
136 | notify(new WampMessage.CallResult(callId, result));
137 |
138 | } else {
139 |
140 | if (DEBUG) Log.d(TAG, "WAMP RPC success return for unknown call ID received");
141 | }
142 |
143 | } else if (msgType == WampMessage.MESSAGE_TYPE_CALL_ERROR) {
144 |
145 | // call ID
146 | parser.nextToken();
147 | String callId = parser.getText();
148 |
149 | // error URI
150 | parser.nextToken();
151 | String errorUri = parser.getText();
152 |
153 | // error description
154 | parser.nextToken();
155 | String errorDesc = parser.getText();
156 |
157 | if (mCalls.containsKey(callId)) {
158 |
159 | notify(new WampMessage.CallError(callId, errorUri, errorDesc));
160 |
161 | } else {
162 |
163 | if (DEBUG) Log.d(TAG, "WAMP RPC error return for unknown call ID received");
164 | }
165 |
166 | } else if (msgType == WampMessage.MESSAGE_TYPE_EVENT) {
167 |
168 | // topic URI
169 | parser.nextToken();
170 | String topicUri = parser.getText();
171 |
172 | // event
173 | parser.nextToken();
174 | Object event = null;
175 |
176 | if (mSubs.containsKey(topicUri)) {
177 |
178 | SubMeta meta = mSubs.get(topicUri);
179 | if (meta.mEventClass != null) {
180 | event = parser.readValueAs(meta.mEventClass);
181 | } else if (meta.mEventTypeRef != null) {
182 | event = parser.readValueAs(meta.mEventTypeRef);
183 | } else {
184 | }
185 | notify(new WampMessage.Event(topicUri, event));
186 |
187 | } else {
188 |
189 | if (DEBUG) Log.d(TAG, "WAMP event for not-subscribed topic received");
190 | }
191 |
192 | } else if (msgType == WampMessage.MESSAGE_TYPE_PREFIX) {
193 |
194 | // prefix
195 | parser.nextToken();
196 | String prefix = parser.getText();
197 |
198 | // URI
199 | parser.nextToken();
200 | String uri = parser.getText();
201 |
202 | notify(new WampMessage.Prefix(prefix, uri));
203 |
204 | } else if (msgType == WampMessage.MESSAGE_TYPE_WELCOME) {
205 |
206 | // session ID
207 | parser.nextToken();
208 | String sessionId = parser.getText();
209 |
210 | // protocol version
211 | parser.nextToken();
212 | int protocolVersion = parser.getIntValue();
213 |
214 | // server ident
215 | parser.nextToken();
216 | String serverIdent = parser.getText();
217 |
218 | notify(new WampMessage.Welcome(sessionId, protocolVersion, serverIdent));
219 |
220 | } else {
221 |
222 | // FIXME: invalid WAMP message
223 | if (DEBUG) Log.d(TAG, "invalid WAMP message: unrecognized message type");
224 |
225 | }
226 | } else {
227 |
228 | if (DEBUG) Log.d(TAG, "invalid WAMP message: missing message type or message type not an integer");
229 | }
230 |
231 | if (parser.nextToken() == JsonToken.END_ARRAY) {
232 |
233 | // nothing to do here
234 |
235 | } else {
236 |
237 | if (DEBUG) Log.d(TAG, "invalid WAMP message: missing array close or invalid additional args");
238 | }
239 |
240 | } else {
241 |
242 | if (DEBUG) Log.d(TAG, "invalid WAMP message: not an array");
243 | }
244 | parser.close();
245 |
246 |
247 | } catch (JsonParseException e) {
248 |
249 | if (DEBUG) e.printStackTrace();
250 |
251 | } catch (IOException e) {
252 |
253 | if (DEBUG) e.printStackTrace();
254 |
255 | }
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WampWriter.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.IOException;
22 | import java.nio.channels.SocketChannel;
23 |
24 | import org.codehaus.jackson.JsonFactory;
25 | import org.codehaus.jackson.JsonGenerationException;
26 | import org.codehaus.jackson.JsonGenerator;
27 | import org.codehaus.jackson.map.JsonMappingException;
28 | import org.codehaus.jackson.map.MappingJsonFactory;
29 |
30 | import android.os.Handler;
31 | import android.os.Looper;
32 | import android.util.Log;
33 |
34 | /**
35 | * Autobahn WAMP writer, the transmitting leg of a WAMP connection.
36 | * This writer serializes WAMP messages forwarded from the foreground thread
37 | * (the master) to this object running on the writer thread. WAMP messages are
38 | * serialized to JSON, and then sent via WebSockets.
39 | */
40 | public class WampWriter extends WebSocketWriter {
41 |
42 | private static final boolean DEBUG = true;
43 | private static final String TAG = WampWriter.class.getName();
44 |
45 | /**
46 | * This is the Jackson JSON factory we use to create JSON generators.
47 | */
48 | private final JsonFactory mJsonFactory;
49 |
50 | /**
51 | * This is where we buffer the JSON serialization of WAMP messages.
52 | */
53 | private final NoCopyByteArrayOutputStream mPayload;
54 |
55 | /**
56 | * A writer object is created in AutobahnConnection.
57 | *
58 | * @param looper The message looper associated with the thread running this object.
59 | * @param master The message handler associated with the master thread (running AutobahnConnection).
60 | * @param socket The TCP socket (channel) the WebSocket connection runs over.
61 | * @param options WebSockets options for the underlying WebSockets connection.
62 | */
63 | public WampWriter(Looper looper, Handler master, SocketChannel socket,
64 | WebSocketOptions options) {
65 |
66 | super(looper, master, socket, options);
67 |
68 | mJsonFactory = new MappingJsonFactory();
69 | mPayload = new NoCopyByteArrayOutputStream();
70 |
71 | if (DEBUG) Log.d(TAG, "created");
72 | }
73 |
74 | /**
75 | * Called from WebSocketWriter when it receives a message in it's
76 | * message loop it does not recognize.
77 | */
78 | protected void processAppMessage(Object msg) throws WebSocketException, IOException {
79 |
80 | mPayload.reset();
81 |
82 | // creating a JSON generator is supposed to be a light-weight operation
83 | JsonGenerator generator = mJsonFactory.createJsonGenerator(mPayload);
84 |
85 | try {
86 |
87 | // serialize WAMP messages to JSON: the code here needs to understand
88 | // any client-to-server WAMP messages forward from the foreground thread
89 |
90 | if (msg instanceof WampMessage.Call) {
91 |
92 | WampMessage.Call call = (WampMessage.Call) msg;
93 |
94 | generator.writeStartArray();
95 | generator.writeNumber(WampMessage.MESSAGE_TYPE_CALL);
96 | generator.writeString(call.mCallId);
97 | generator.writeString(call.mProcUri);
98 | for (Object arg : call.mArgs) {
99 | generator.writeObject(arg);
100 | }
101 | generator.writeEndArray();
102 |
103 | } else if (msg instanceof WampMessage.Prefix) {
104 |
105 | WampMessage.Prefix prefix = (WampMessage.Prefix) msg;
106 |
107 | generator.writeStartArray();
108 | generator.writeNumber(WampMessage.MESSAGE_TYPE_PREFIX);
109 | generator.writeString(prefix.mPrefix);
110 | generator.writeString(prefix.mUri);
111 | generator.writeEndArray();
112 |
113 | } else if (msg instanceof WampMessage.Subscribe) {
114 |
115 | WampMessage.Subscribe subscribe = (WampMessage.Subscribe) msg;
116 |
117 | generator.writeStartArray();
118 | generator.writeNumber(WampMessage.MESSAGE_TYPE_SUBSCRIBE);
119 | generator.writeString(subscribe.mTopicUri);
120 | generator.writeEndArray();
121 |
122 | } else if (msg instanceof WampMessage.Unsubscribe) {
123 |
124 | WampMessage.Unsubscribe unsubscribe = (WampMessage.Unsubscribe) msg;
125 |
126 | generator.writeStartArray();
127 | generator.writeNumber(WampMessage.MESSAGE_TYPE_UNSUBSCRIBE);
128 | generator.writeString(unsubscribe.mTopicUri);
129 | generator.writeEndArray();
130 |
131 | } else if (msg instanceof WampMessage.Publish) {
132 |
133 | WampMessage.Publish publish = (WampMessage.Publish) msg;
134 |
135 | generator.writeStartArray();
136 | generator.writeNumber(WampMessage.MESSAGE_TYPE_PUBLISH);
137 | generator.writeString(publish.mTopicUri);
138 | generator.writeObject(publish.mEvent);
139 | generator.writeEndArray();
140 |
141 | } else {
142 |
143 | // this should not happen, but to be sure
144 | throw new WebSocketException("invalid message received by AutobahnWriter");
145 | }
146 | } catch (JsonGenerationException e) {
147 |
148 | // this may happen, and we need to wrap the error
149 | throw new WebSocketException("JSON serialization error (" + e.toString() + ")");
150 |
151 | } catch (JsonMappingException e) {
152 |
153 | // this may happen, and we need to wrap the error
154 | throw new WebSocketException("JSON serialization error (" + e.toString() + ")");
155 | }
156 |
157 | // make sure the JSON generator has spit out everything
158 | generator.flush();
159 |
160 | // Jackson's JSON generator produces UTF-8 directly, so we send
161 | // a text message frame using the raw sendFrame() method
162 | sendFrame(1, true, mPayload.getByteArray(), 0, mPayload.size());
163 |
164 | // cleanup generators resources
165 | generator.close();
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocket.java:
--------------------------------------------------------------------------------
1 | package de.tavendo.autobahn;
2 |
3 | public interface WebSocket {
4 |
5 | /**
6 | * Session handler for WebSocket sessions.
7 | */
8 | public interface ConnectionHandler {
9 |
10 | /**
11 | * Connection was closed normally.
12 | */
13 | public static final int CLOSE_NORMAL = 1;
14 |
15 | /**
16 | * Connection could not be established in the first place.
17 | */
18 | public static final int CLOSE_CANNOT_CONNECT = 2;
19 |
20 | /**
21 | * A previously established connection was lost unexpected.
22 | */
23 | public static final int CLOSE_CONNECTION_LOST = 3;
24 |
25 | /**
26 | * The connection was closed because a protocol violation
27 | * occurred.
28 | */
29 | public static final int CLOSE_PROTOCOL_ERROR = 4;
30 |
31 | /**
32 | * Internal error.
33 | */
34 | public static final int CLOSE_INTERNAL_ERROR = 5;
35 |
36 | /**
37 | * Server returned error while connecting
38 | */
39 | public static final int CLOSE_SERVER_ERROR = 6;
40 |
41 | /**
42 | * Server connection lost, scheduled reconnect
43 | */
44 | public static final int CLOSE_RECONNECT = 7;
45 |
46 | /**
47 | * Fired when the WebSockets connection has been established.
48 | * After this happened, messages may be sent.
49 | */
50 | public void onOpen();
51 |
52 | /**
53 | * Fired when the WebSockets connection has deceased (or could
54 | * not established in the first place).
55 | *
56 | * @param code Close code.
57 | * @param reason Close reason (human-readable).
58 | */
59 | public void onClose(int code, String reason);
60 |
61 | /**
62 | * Fired when a text message has been received (and text
63 | * messages are not set to be received raw).
64 | *
65 | * @param payload Text message payload or null (empty payload).
66 | */
67 | public void onTextMessage(String payload);
68 |
69 | /**
70 | * Fired when a text message has been received (and text
71 | * messages are set to be received raw).
72 | *
73 | * @param payload Text message payload as raw UTF-8 or null (empty payload).
74 | */
75 | public void onRawTextMessage(byte[] payload);
76 |
77 | /**
78 | * Fired when a binary message has been received.
79 | *
80 | * @param payload Binar message payload or null (empty payload).
81 | */
82 | public void onBinaryMessage(byte[] payload);
83 | }
84 |
85 | public void connect(String wsUri, ConnectionHandler wsHandler) throws WebSocketException;
86 | public void connect(String wsUri, ConnectionHandler wsHandler, WebSocketOptions options) throws WebSocketException;
87 | public void disconnect();
88 | public boolean isConnected();
89 | public void sendBinaryMessage(byte[] payload);
90 | public void sendRawTextMessage(byte[] payload);
91 | public void sendTextMessage(String payload);
92 | }
93 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketConnection.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.IOException;
22 | import java.net.InetSocketAddress;
23 | import java.net.URI;
24 | import java.net.URISyntaxException;
25 | import java.nio.channels.SocketChannel;
26 |
27 | import com.moko365.android.websocket.WebsocketHandler;
28 |
29 | import android.os.AsyncTask;
30 | import android.os.Handler;
31 | import android.os.HandlerThread;
32 | import android.os.Message;
33 | import android.util.Log;
34 |
35 | public class WebSocketConnection implements WebSocket {
36 |
37 | private static final boolean DEBUG = true;
38 | private static final String TAG = WebSocketConnection.class.getName();
39 |
40 | protected Handler mMasterHandler;
41 |
42 | protected WebSocketReader mReader;
43 | protected WebSocketWriter mWriter;
44 | protected HandlerThread mWriterThread;
45 |
46 | protected SocketChannel mTransportChannel;
47 |
48 | private URI mWsUri;
49 | private String mWsScheme;
50 | private String mWsHost;
51 | private int mWsPort;
52 | private String mWsPath;
53 | private String mWsQuery;
54 | private String mWsSubprotocols;
55 |
56 | private WebSocket.ConnectionHandler mWsHandler;
57 |
58 | protected WebSocketOptions mOptions;
59 |
60 | private boolean mActive;
61 | private boolean mPrevConnected;
62 |
63 | /**
64 | * Asynch socket connector.
65 | */
66 | private class WebSocketConnector extends AsyncTask {
67 |
68 | @Override
69 | protected String doInBackground(Void... params) {
70 |
71 | Thread.currentThread().setName("WebSocketConnector");
72 |
73 | // connect TCP socket
74 | // http://developer.android.com/reference/java/nio/channels/SocketChannel.html
75 | //
76 | try {
77 | mTransportChannel = SocketChannel.open();
78 |
79 | // the following will block until connection was established or an error occurred!
80 | mTransportChannel.socket().connect(new InetSocketAddress(mWsHost, mWsPort), mOptions.getSocketConnectTimeout());
81 |
82 | // before doing any data transfer on the socket, set socket options
83 | mTransportChannel.socket().setSoTimeout(mOptions.getSocketReceiveTimeout());
84 | mTransportChannel.socket().setTcpNoDelay(mOptions.getTcpNoDelay());
85 |
86 | return null;
87 |
88 | } catch (IOException e) {
89 |
90 | return e.getMessage();
91 | }
92 | }
93 |
94 | @Override
95 | protected void onPostExecute(String reason) {
96 |
97 | if (reason != null) {
98 |
99 | onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT, reason);
100 |
101 | } else if (mTransportChannel.isConnected()) {
102 |
103 | try {
104 |
105 | // create & start WebSocket reader
106 | createReader();
107 |
108 | // create & start WebSocket writer
109 | createWriter();
110 |
111 | // start WebSockets handshake
112 | WebSocketMessage.ClientHandshake hs = new WebSocketMessage.ClientHandshake(mWsHost + ":" + mWsPort);
113 | hs.mPath = mWsPath;
114 | hs.mQuery = mWsQuery;
115 | hs.mSubprotocols = mWsSubprotocols;
116 | mWriter.forward(hs);
117 |
118 | mPrevConnected = true;
119 |
120 | } catch (Exception e) {
121 |
122 | onClose(WebSocketConnectionHandler.CLOSE_INTERNAL_ERROR, e.getMessage());
123 |
124 | }
125 |
126 | } else {
127 |
128 | onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT, "could not connect to WebSockets server");
129 | }
130 | }
131 |
132 | }
133 |
134 |
135 | public WebSocketConnection() {
136 | if (DEBUG) Log.d(TAG, "created");
137 |
138 | // create WebSocket master handler
139 | createHandler();
140 |
141 | // set initial values
142 | mActive = false;
143 | mPrevConnected = false;
144 | }
145 |
146 |
147 | public void sendTextMessage(String payload) {
148 | mWriter.forward(new WebSocketMessage.TextMessage(payload));
149 | }
150 |
151 |
152 | public void sendRawTextMessage(byte[] payload) {
153 | mWriter.forward(new WebSocketMessage.RawTextMessage(payload));
154 | }
155 |
156 |
157 | public void sendBinaryMessage(byte[] payload) {
158 | mWriter.forward(new WebSocketMessage.BinaryMessage(payload));
159 | }
160 |
161 |
162 | public boolean isConnected() {
163 | return mTransportChannel != null && mTransportChannel.isConnected();
164 | }
165 |
166 |
167 | private void failConnection(int code, String reason) {
168 |
169 | if (DEBUG) Log.d(TAG, "fail connection [code = " + code + ", reason = " + reason);
170 |
171 | if (mReader != null) {
172 | mReader.quit();
173 | try {
174 | mReader.join();
175 | } catch (InterruptedException e) {
176 | if (DEBUG) e.printStackTrace();
177 | }
178 | //mReader = null;
179 | } else {
180 | if (DEBUG) Log.d(TAG, "mReader already NULL");
181 | }
182 |
183 | if (mWriter != null) {
184 | //mWriterThread.getLooper().quit();
185 | mWriter.forward(new WebSocketMessage.Quit());
186 | try {
187 | mWriterThread.join();
188 | } catch (InterruptedException e) {
189 | if (DEBUG) e.printStackTrace();
190 | }
191 | //mWriterThread = null;
192 | } else {
193 | if (DEBUG) Log.d(TAG, "mWriter already NULL");
194 | }
195 |
196 | if (mTransportChannel != null) {
197 | try {
198 | mTransportChannel.close();
199 | } catch (IOException e) {
200 | if (DEBUG) e.printStackTrace();
201 | }
202 | //mTransportChannel = null;
203 | } else {
204 | if (DEBUG) Log.d(TAG, "mTransportChannel already NULL");
205 | }
206 |
207 | onClose(code, reason);
208 |
209 | if (DEBUG) Log.d(TAG, "worker threads stopped");
210 | }
211 |
212 |
213 | public void connect(String wsUri, WebSocket.ConnectionHandler wsHandler) throws WebSocketException {
214 | connect(wsUri, null, wsHandler, new WebSocketOptions());
215 | }
216 |
217 |
218 | public void connect(String wsUri, WebSocket.ConnectionHandler wsHandler, WebSocketOptions options) throws WebSocketException {
219 | connect(wsUri, null, wsHandler, options);
220 | }
221 |
222 | public void connect(String wsUri, String wsProtocol, WebsocketHandler wsHandler) throws WebSocketException {
223 | connect(wsUri, wsProtocol, wsHandler, new WebSocketOptions());
224 | }
225 |
226 | public void connect(String wsUri, String wsSubprotocols, WebSocket.ConnectionHandler wsHandler, WebSocketOptions options) throws WebSocketException {
227 |
228 | // don't connect if already connected .. user needs to disconnect first
229 | //
230 | if (mTransportChannel != null && mTransportChannel.isConnected()) {
231 | throw new WebSocketException("already connected");
232 | }
233 |
234 | // parse WebSockets URI
235 | //
236 | try {
237 | mWsUri = new URI(wsUri);
238 |
239 | if (!mWsUri.getScheme().equals("ws") && !mWsUri.getScheme().equals("wss")) {
240 | throw new WebSocketException("unsupported scheme for WebSockets URI");
241 | }
242 |
243 | if (mWsUri.getScheme().equals("wss")) {
244 | throw new WebSocketException("secure WebSockets not implemented");
245 | }
246 |
247 | mWsScheme = mWsUri.getScheme();
248 |
249 | if (mWsUri.getPort() == -1) {
250 | if (mWsScheme.equals("ws")) {
251 | mWsPort = 80;
252 | } else {
253 | mWsPort = 443;
254 | }
255 | } else {
256 | mWsPort = mWsUri.getPort();
257 | }
258 |
259 | if (mWsUri.getHost() == null) {
260 | throw new WebSocketException("no host specified in WebSockets URI");
261 | } else {
262 | mWsHost = mWsUri.getHost();
263 | }
264 |
265 | if (mWsUri.getPath() == null || mWsUri.getPath().equals("")) {
266 | mWsPath = "/";
267 | } else {
268 | mWsPath = mWsUri.getPath();
269 | }
270 |
271 | if (mWsUri.getQuery() == null || mWsUri.getQuery().equals("")) {
272 | mWsQuery = null;
273 | } else {
274 | mWsQuery = mWsUri.getQuery();
275 | }
276 |
277 | } catch (URISyntaxException e) {
278 |
279 | throw new WebSocketException("invalid WebSockets URI");
280 | }
281 |
282 | mWsSubprotocols = wsSubprotocols;
283 |
284 | mWsHandler = wsHandler;
285 |
286 | // make copy of options!
287 | mOptions = new WebSocketOptions(options);
288 |
289 | // set connection active
290 | mActive = true;
291 |
292 | // use asynch connector on short-lived background thread
293 | new WebSocketConnector().execute();
294 | }
295 |
296 |
297 | public void disconnect() {
298 | if (mWriter != null) {
299 | mWriter.forward(new WebSocketMessage.Close(1000));
300 | } else {
301 | if (DEBUG) Log.d(TAG, "could not send Close .. writer already NULL");
302 | }
303 | mActive = false;
304 | mPrevConnected = false;
305 | }
306 |
307 | /**
308 | * Reconnect to the server with the latest options
309 | * @return true if reconnection performed
310 | */
311 | public boolean reconnect() {
312 | if (!isConnected() && (mWsUri != null)) {
313 | new WebSocketConnector().execute();
314 | return true;
315 | }
316 | return false;
317 | }
318 |
319 | /**
320 | * Perform reconnection
321 | *
322 | * @return true if reconnection was scheduled
323 | */
324 | protected boolean scheduleReconnect() {
325 | /**
326 | * Reconnect only if:
327 | * - connection active (connected but not disconnected)
328 | * - has previous success connections
329 | * - reconnect interval is set
330 | */
331 | int interval = mOptions.getReconnectInterval();
332 | boolean need = mActive && mPrevConnected && (interval > 0);
333 | if (need) {
334 | if (DEBUG) Log.d(TAG, "Reconnection scheduled");
335 | mMasterHandler.postDelayed(new Runnable() {
336 |
337 | public void run() {
338 | if (DEBUG) Log.d(TAG, "Reconnecting...");
339 | reconnect();
340 | }
341 | }, interval);
342 | }
343 | return need;
344 | }
345 |
346 | /**
347 | * Common close handler
348 | *
349 | * @param code Close code.
350 | * @param reason Close reason (human-readable).
351 | */
352 | private void onClose(int code, String reason) {
353 | boolean reconnecting = false;
354 |
355 | if ((code == WebSocket.ConnectionHandler.CLOSE_CANNOT_CONNECT) ||
356 | (code == WebSocket.ConnectionHandler.CLOSE_CONNECTION_LOST)) {
357 | reconnecting = scheduleReconnect();
358 | }
359 |
360 |
361 | if (mWsHandler != null) {
362 | try {
363 | if (reconnecting) {
364 | mWsHandler.onClose(WebSocket.ConnectionHandler.CLOSE_RECONNECT, reason);
365 | } else {
366 | mWsHandler.onClose(code, reason);
367 | }
368 | } catch (Exception e) {
369 | if (DEBUG) e.printStackTrace();
370 | }
371 | //mWsHandler = null;
372 | } else {
373 | if (DEBUG) Log.d(TAG, "mWsHandler already NULL");
374 | }
375 | }
376 |
377 |
378 | /**
379 | * Create master message handler.
380 | */
381 | protected void createHandler() {
382 |
383 | mMasterHandler = new Handler() {
384 |
385 | public void handleMessage(Message msg) {
386 |
387 | if (msg.obj instanceof WebSocketMessage.TextMessage) {
388 |
389 | WebSocketMessage.TextMessage textMessage = (WebSocketMessage.TextMessage) msg.obj;
390 |
391 | if (mWsHandler != null) {
392 | mWsHandler.onTextMessage(textMessage.mPayload);
393 | } else {
394 | if (DEBUG) Log.d(TAG, "could not call onTextMessage() .. handler already NULL");
395 | }
396 |
397 | } else if (msg.obj instanceof WebSocketMessage.RawTextMessage) {
398 |
399 | WebSocketMessage.RawTextMessage rawTextMessage = (WebSocketMessage.RawTextMessage) msg.obj;
400 |
401 | if (mWsHandler != null) {
402 | mWsHandler.onRawTextMessage(rawTextMessage.mPayload);
403 | } else {
404 | if (DEBUG) Log.d(TAG, "could not call onRawTextMessage() .. handler already NULL");
405 | }
406 |
407 | } else if (msg.obj instanceof WebSocketMessage.BinaryMessage) {
408 |
409 | WebSocketMessage.BinaryMessage binaryMessage = (WebSocketMessage.BinaryMessage) msg.obj;
410 |
411 | if (mWsHandler != null) {
412 | mWsHandler.onBinaryMessage(binaryMessage.mPayload);
413 | } else {
414 | if (DEBUG) Log.d(TAG, "could not call onBinaryMessage() .. handler already NULL");
415 | }
416 |
417 | } else if (msg.obj instanceof WebSocketMessage.Ping) {
418 |
419 | WebSocketMessage.Ping ping = (WebSocketMessage.Ping) msg.obj;
420 | if (DEBUG) Log.d(TAG, "WebSockets Ping received");
421 |
422 | // reply with Pong
423 | WebSocketMessage.Pong pong = new WebSocketMessage.Pong();
424 | pong.mPayload = ping.mPayload;
425 | mWriter.forward(pong);
426 |
427 | } else if (msg.obj instanceof WebSocketMessage.Pong) {
428 |
429 | @SuppressWarnings("unused")
430 | WebSocketMessage.Pong pong = (WebSocketMessage.Pong) msg.obj;
431 |
432 | if (DEBUG) Log.d(TAG, "WebSockets Pong received");
433 |
434 | } else if (msg.obj instanceof WebSocketMessage.Close) {
435 |
436 | WebSocketMessage.Close close = (WebSocketMessage.Close) msg.obj;
437 |
438 | if (DEBUG) Log.d(TAG, "WebSockets Close received (" + close.mCode + " - " + close.mReason + ")");
439 |
440 | mWriter.forward(new WebSocketMessage.Close(1000));
441 |
442 | } else if (msg.obj instanceof WebSocketMessage.ServerHandshake) {
443 |
444 | WebSocketMessage.ServerHandshake serverHandshake = (WebSocketMessage.ServerHandshake) msg.obj;
445 |
446 | if (DEBUG) Log.d(TAG, "opening handshake received");
447 |
448 | if (serverHandshake.mSuccess) {
449 | if (mWsHandler != null) {
450 | mWsHandler.onOpen();
451 | } else {
452 | if (DEBUG) Log.d(TAG, "could not call onOpen() .. handler already NULL");
453 | }
454 | }
455 |
456 | } else if (msg.obj instanceof WebSocketMessage.ConnectionLost) {
457 |
458 | @SuppressWarnings("unused")
459 | WebSocketMessage.ConnectionLost connnectionLost = (WebSocketMessage.ConnectionLost) msg.obj;
460 | failConnection(WebSocketConnectionHandler.CLOSE_CONNECTION_LOST, "WebSockets connection lost");
461 |
462 | } else if (msg.obj instanceof WebSocketMessage.ProtocolViolation) {
463 |
464 | @SuppressWarnings("unused")
465 | WebSocketMessage.ProtocolViolation protocolViolation = (WebSocketMessage.ProtocolViolation) msg.obj;
466 | failConnection(WebSocketConnectionHandler.CLOSE_PROTOCOL_ERROR, "WebSockets protocol violation");
467 |
468 | } else if (msg.obj instanceof WebSocketMessage.Error) {
469 |
470 | WebSocketMessage.Error error = (WebSocketMessage.Error) msg.obj;
471 | failConnection(WebSocketConnectionHandler.CLOSE_INTERNAL_ERROR, "WebSockets internal error (" + error.mException.toString() + ")");
472 |
473 | } else if (msg.obj instanceof WebSocketMessage.ServerError) {
474 |
475 | WebSocketMessage.ServerError error = (WebSocketMessage.ServerError) msg.obj;
476 | failConnection(WebSocketConnectionHandler.CLOSE_SERVER_ERROR, "Server error " + error.mStatusCode + " (" + error.mStatusMessage + ")");
477 |
478 | } else {
479 |
480 | processAppMessage(msg.obj);
481 |
482 | }
483 | }
484 | };
485 | }
486 |
487 |
488 | protected void processAppMessage(Object message) {
489 | }
490 |
491 |
492 | /**
493 | * Create WebSockets background writer.
494 | */
495 | protected void createWriter() {
496 |
497 | mWriterThread = new HandlerThread("WebSocketWriter");
498 | mWriterThread.start();
499 | mWriter = new WebSocketWriter(mWriterThread.getLooper(), mMasterHandler, mTransportChannel, mOptions);
500 |
501 | if (DEBUG) Log.d(TAG, "WS writer created and started");
502 | }
503 |
504 |
505 | /**
506 | * Create WebSockets background reader.
507 | */
508 | protected void createReader() {
509 |
510 | mReader = new WebSocketReader(mMasterHandler, mTransportChannel, mOptions, "WebSocketReader");
511 | mReader.start();
512 |
513 | if (DEBUG) Log.d(TAG, "WS reader created and started");
514 | }
515 | }
516 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketConnectionHandler.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | /**
22 | * WebSockets event handler. Users will usually provide an instance of a class
23 | * derived from this to handle WebSockets received messages and open/close events
24 | */
25 | public class WebSocketConnectionHandler implements WebSocket.ConnectionHandler {
26 |
27 | /**
28 | * Fired when the WebSockets connection has been established.
29 | * After this happened, messages may be sent.
30 | */
31 | public void onOpen() {
32 | }
33 |
34 | /**
35 | * Fired when the WebSockets connection has deceased (or could
36 | * not established in the first place).
37 | *
38 | * @param code Close code.
39 | * @param reason Close reason (human-readable).
40 | */
41 | public void onClose(int code, String reason) {
42 | }
43 |
44 | /**
45 | * Fired when a text message has been received (and text
46 | * messages are not set to be received raw).
47 | *
48 | * @param payload Text message payload or null (empty payload).
49 | */
50 | public void onTextMessage(String payload) {
51 | }
52 |
53 | /**
54 | * Fired when a text message has been received (and text
55 | * messages are set to be received raw).
56 | *
57 | * @param payload Text message payload as raw UTF-8 or null (empty payload).
58 | */
59 | public void onRawTextMessage(byte[] payload) {
60 | }
61 |
62 | /**
63 | * Fired when a binary message has been received.
64 | *
65 | * @param payload Binar message payload or null (empty payload).
66 | */
67 | public void onBinaryMessage(byte[] payload) {
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketException.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | public class WebSocketException extends Exception {
22 |
23 | private static final long serialVersionUID = 1L;
24 |
25 | public WebSocketException(String message) {
26 | super(message);
27 | }
28 |
29 | public WebSocketException(String message, Throwable t) {
30 | super(message, t);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketMessage.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | /**
22 | * WebSockets message classes.
23 | * The master thread and the background reader/writer threads communicate using these messages
24 | * for WebSockets connections.
25 | */
26 | public class WebSocketMessage {
27 |
28 | /// Base message class.
29 | public static class Message {
30 | }
31 |
32 | /// Quite background thread.
33 | public static class Quit extends Message {
34 | }
35 |
36 | /// Initial WebSockets handshake (client request).
37 | public static class ClientHandshake extends Message {
38 |
39 | public String mHost;
40 | public String mPath;
41 | public String mQuery;
42 | public String mOrigin;
43 | public String mSubprotocols;
44 |
45 | ClientHandshake(String host) {
46 | mHost = host;
47 | mPath = "/";
48 | mOrigin = null;
49 | mSubprotocols = null;
50 | }
51 |
52 | ClientHandshake(String host, String path, String origin) {
53 | mHost = host;
54 | mPath = path;
55 | mOrigin = origin;
56 | mSubprotocols = null;
57 | }
58 |
59 | ClientHandshake(String host, String path, String origin, String subprotocols) {
60 | mHost = host;
61 | mPath = path;
62 | mOrigin = origin;
63 | mSubprotocols = subprotocols;
64 | }
65 | }
66 |
67 | /// Initial WebSockets handshake (server response).
68 | public static class ServerHandshake extends Message {
69 | public boolean mSuccess;
70 |
71 | public ServerHandshake(boolean success) {
72 | mSuccess = success;
73 | }
74 | }
75 |
76 | /// WebSockets connection lost
77 | public static class ConnectionLost extends Message {
78 | }
79 |
80 | public static class ServerError extends Message {
81 | public int mStatusCode;
82 | public String mStatusMessage;
83 |
84 | public ServerError(int statusCode, String statusMessage) {
85 | mStatusCode = statusCode;
86 | mStatusMessage = statusMessage;
87 | }
88 |
89 | }
90 |
91 | /// WebSockets reader detected WS protocol violation.
92 | public static class ProtocolViolation extends Message {
93 |
94 | public WebSocketException mException;
95 |
96 | public ProtocolViolation(WebSocketException e) {
97 | mException = e;
98 | }
99 | }
100 |
101 | /// An exception occured in the WS reader or WS writer.
102 | public static class Error extends Message {
103 |
104 | public Exception mException;
105 |
106 | public Error(Exception e) {
107 | mException = e;
108 | }
109 | }
110 |
111 | /// WebSockets text message to send or received.
112 | public static class TextMessage extends Message {
113 |
114 | public String mPayload;
115 |
116 | TextMessage(String payload) {
117 | mPayload = payload;
118 | }
119 | }
120 |
121 | /// WebSockets raw (UTF-8) text message to send or received.
122 | public static class RawTextMessage extends Message {
123 |
124 | public byte[] mPayload;
125 |
126 | RawTextMessage(byte[] payload) {
127 | mPayload = payload;
128 | }
129 | }
130 |
131 | /// WebSockets binary message to send or received.
132 | public static class BinaryMessage extends Message {
133 |
134 | public byte[] mPayload;
135 |
136 | BinaryMessage(byte[] payload) {
137 | mPayload = payload;
138 | }
139 | }
140 |
141 | /// WebSockets close to send or received.
142 | public static class Close extends Message {
143 |
144 | public int mCode;
145 | public String mReason;
146 |
147 | Close() {
148 | mCode = -1;
149 | mReason = null;
150 | }
151 |
152 | Close(int code) {
153 | mCode = code;
154 | mReason = null;
155 | }
156 |
157 | Close(int code, String reason) {
158 | mCode = code;
159 | mReason = reason;
160 | }
161 | }
162 |
163 | /// WebSockets ping to send or received.
164 | public static class Ping extends Message {
165 |
166 | public byte[] mPayload;
167 |
168 | Ping() {
169 | mPayload = null;
170 | }
171 |
172 | Ping(byte[] payload) {
173 | mPayload = payload;
174 | }
175 | }
176 |
177 | /// WebSockets pong to send or received.
178 | public static class Pong extends Message {
179 |
180 | public byte[] mPayload;
181 |
182 | Pong() {
183 | mPayload = null;
184 | }
185 |
186 | Pong(byte[] payload) {
187 | mPayload = payload;
188 | }
189 | }
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketOptions.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 |
22 |
23 | /**
24 | * WebSockets connection options. This can be supplied to WebSocketConnection in connect().
25 | * Note that the latter copies the options provided to connect(), so any change after
26 | * connect will have no effect.
27 | */
28 | public class WebSocketOptions {
29 |
30 | private int mMaxFramePayloadSize;
31 | private int mMaxMessagePayloadSize;
32 | private boolean mReceiveTextMessagesRaw;
33 | private boolean mTcpNoDelay;
34 | private int mSocketReceiveTimeout;
35 | private int mSocketConnectTimeout;
36 | private boolean mValidateIncomingUtf8;
37 | private boolean mMaskClientFrames;
38 | private int mReconnectInterval;
39 |
40 |
41 | /**
42 | * Construct default options.
43 | */
44 | public WebSocketOptions() {
45 |
46 | mMaxFramePayloadSize = 128 * 1024;
47 | mMaxMessagePayloadSize = 128 * 1024;
48 | mReceiveTextMessagesRaw = false;
49 | mTcpNoDelay = true;
50 | mSocketReceiveTimeout = 200;
51 | mSocketConnectTimeout = 6000;
52 | mValidateIncomingUtf8 = true;
53 | mMaskClientFrames = true;
54 | mReconnectInterval = 0; // no reconnection by default
55 | }
56 |
57 | /**
58 | * Construct options as copy from other options object.
59 | *
60 | * @param other Options to copy.
61 | */
62 | public WebSocketOptions(WebSocketOptions other) {
63 |
64 | mMaxFramePayloadSize = other.mMaxFramePayloadSize;
65 | mMaxMessagePayloadSize = other.mMaxMessagePayloadSize;
66 | mReceiveTextMessagesRaw = other.mReceiveTextMessagesRaw;
67 | mTcpNoDelay = other.mTcpNoDelay;
68 | mSocketReceiveTimeout = other.mSocketReceiveTimeout;
69 | mSocketConnectTimeout = other.mSocketConnectTimeout;
70 | mValidateIncomingUtf8 = other.mValidateIncomingUtf8;
71 | mMaskClientFrames = other.mMaskClientFrames;
72 | mReconnectInterval = other.mReconnectInterval;
73 | }
74 |
75 | /**
76 | * Receive text message as raw byte array with verified,
77 | * but non-decoded UTF-8.
78 | *
79 | * DEFAULT: false
80 | *
81 | * @param enabled True to enable.
82 | */
83 | public void setReceiveTextMessagesRaw(boolean enabled) {
84 | mReceiveTextMessagesRaw = enabled;
85 | }
86 |
87 | /**
88 | * When true, WebSockets text messages are provided as
89 | * verified, but non-decoded UTF-8 in byte arrays.
90 | *
91 | * @return True, iff option is enabled.
92 | */
93 | public boolean getReceiveTextMessagesRaw() {
94 | return mReceiveTextMessagesRaw;
95 | }
96 |
97 | /**
98 | * Set maximum frame payload size that will be accepted
99 | * when receiving.
100 | *
101 | * DEFAULT: 4MB
102 | *
103 | * @param size Maximum size in octets for frame payload.
104 | */
105 | public void setMaxFramePayloadSize(int size) {
106 | if (size > 0) {
107 | mMaxFramePayloadSize = size;
108 | if (mMaxMessagePayloadSize < mMaxFramePayloadSize) {
109 | mMaxMessagePayloadSize = mMaxFramePayloadSize;
110 | }
111 | }
112 | }
113 |
114 | /**
115 | * Get maxium frame payload size that will be accepted
116 | * when receiving.
117 | *
118 | * @return Maximum size in octets for frame payload.
119 | */
120 | public int getMaxFramePayloadSize() {
121 | return mMaxFramePayloadSize;
122 | }
123 |
124 | /**
125 | * Set maximum message payload size (after reassembly of fragmented
126 | * messages) that will be accepted when receiving.
127 | *
128 | * DEFAULT: 4MB
129 | *
130 | * @param size Maximum size in octets for message payload.
131 | */
132 | public void setMaxMessagePayloadSize(int size) {
133 | if (size > 0) {
134 | mMaxMessagePayloadSize = size;
135 | if (mMaxMessagePayloadSize < mMaxFramePayloadSize) {
136 | mMaxFramePayloadSize = mMaxMessagePayloadSize;
137 | }
138 | }
139 | }
140 |
141 | /**
142 | * Get maximum message payload size (after reassembly of fragmented
143 | * messages) that will be accepted when receiving.
144 | *
145 | * @return Maximum size in octets for message payload.
146 | */
147 | public int getMaxMessagePayloadSize() {
148 | return mMaxMessagePayloadSize;
149 | }
150 |
151 | /**
152 | * Set TCP No-Delay ("Nagle") for TCP connection.
153 | *
154 | * DEFAULT: true
155 | *
156 | * @param enabled True to enable TCP No-Delay.
157 | */
158 | public void setTcpNoDelay(boolean enabled) {
159 | mTcpNoDelay = enabled;
160 | }
161 |
162 | /**
163 | * Get TCP No-Delay ("Nagle") for TCP connection.
164 | *
165 | * @return True, iff TCP No-Delay is enabled.
166 | */
167 | public boolean getTcpNoDelay() {
168 | return mTcpNoDelay;
169 | }
170 |
171 | /**
172 | * Set receive timeout on socket. When the TCP connection disappears,
173 | * that will only be recognized by the reader after this timeout.
174 | *
175 | * DEFAULT: 200
176 | *
177 | * @param timeoutMs Socket receive timeout in ms.
178 | */
179 | public void setSocketReceiveTimeout(int timeoutMs) {
180 | if (timeoutMs >= 0) {
181 | mSocketReceiveTimeout = timeoutMs;
182 | }
183 | }
184 |
185 | /**
186 | * Get socket receive timeout.
187 | *
188 | * @return Socket receive timeout in ms.
189 | */
190 | public int getSocketReceiveTimeout() {
191 | return mSocketReceiveTimeout;
192 | }
193 |
194 | /**
195 | * Set connect timeout on socket. When a WebSocket connection is
196 | * about to be established, the TCP socket connect will timeout
197 | * after this period.
198 | *
199 | * DEFAULT: 3000
200 | *
201 | * @param timeoutMs Socket connect timeout in ms.
202 | */
203 | public void setSocketConnectTimeout(int timeoutMs) {
204 | if (timeoutMs >= 0) {
205 | mSocketConnectTimeout = timeoutMs;
206 | }
207 | }
208 |
209 | /**
210 | * Get socket connect timeout.
211 | *
212 | * @return Socket receive timeout in ms.
213 | */
214 | public int getSocketConnectTimeout() {
215 | return mSocketConnectTimeout;
216 | }
217 |
218 | /**
219 | * Controls whether incoming text message payload is verified
220 | * to be valid UTF-8.
221 | *
222 | * DEFAULT: true
223 | *
224 | * @param enabled True to verify incoming UTF-8.
225 | */
226 | public void setValidateIncomingUtf8(boolean enabled) {
227 | mValidateIncomingUtf8 = enabled;
228 | }
229 |
230 | /**
231 | * Get UTF-8 validation option.
232 | *
233 | * @return True, iff incoming UTF-8 is validated.
234 | */
235 | public boolean getValidateIncomingUtf8() {
236 | return mValidateIncomingUtf8;
237 | }
238 |
239 | /**
240 | * Controls whether to mask client-to-server WebSocket frames.
241 | * Beware, normally, WebSockets servers will deny non-masked c2s
242 | * frames and fail the connection.
243 | *
244 | * DEFAULT: true
245 | *
246 | * @param enabled Set true to mask client-to-server frames.
247 | */
248 | public void setMaskClientFrames(boolean enabled) {
249 | mMaskClientFrames = enabled;
250 | }
251 |
252 | /**
253 | * Get mask client frames option.
254 | *
255 | * @return True, iff client-to-server frames are masked.
256 | */
257 | public boolean getMaskClientFrames() {
258 | return mMaskClientFrames;
259 | }
260 |
261 | /**
262 | * Set reconnect interval
263 | *
264 | * @param reconnectInterval Interval in ms, 0 - no reconnection
265 | */
266 | public void setReconnectInterval(int reconnectInterval) {
267 | mReconnectInterval = reconnectInterval;
268 | }
269 |
270 | public int getReconnectInterval() {
271 | return mReconnectInterval;
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketReader.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.UnsupportedEncodingException;
22 | import java.net.SocketException;
23 | import java.nio.ByteBuffer;
24 | import java.nio.channels.SocketChannel;
25 | import java.util.HashMap;
26 | import java.util.Map;
27 |
28 | import android.os.Handler;
29 | import android.os.Message;
30 | import android.util.Log;
31 | import android.util.Pair;
32 |
33 | /**
34 | * WebSocket reader, the receiving leg of a WebSockets connection.
35 | * This runs on it's own background thread and posts messages to master
36 | * thread's message queue for there to be consumed by the application.
37 | * The only method that needs to be called (from foreground thread) is quit(),
38 | * which gracefully shuts down the background receiver thread.
39 | */
40 | public class WebSocketReader extends Thread {
41 |
42 | private static final boolean DEBUG = true;
43 | private static final String TAG = WebSocketReader.class.getName();
44 |
45 | private final Handler mMaster;
46 | private final SocketChannel mSocket;
47 | private final WebSocketOptions mOptions;
48 |
49 | private final ByteBuffer mFrameBuffer;
50 | private NoCopyByteArrayOutputStream mMessagePayload;
51 |
52 | private final static int STATE_CLOSED = 0;
53 | private final static int STATE_CONNECTING = 1;
54 | private final static int STATE_CLOSING = 2;
55 | private final static int STATE_OPEN = 3;
56 |
57 | private boolean mStopped = false;
58 | private int mState;
59 |
60 | private boolean mInsideMessage = false;
61 | private int mMessageOpcode;
62 |
63 | /// Frame currently being received.
64 | private FrameHeader mFrameHeader;
65 |
66 | private Utf8Validator mUtf8Validator = new Utf8Validator();
67 |
68 |
69 | /**
70 | * WebSockets frame metadata.
71 | */
72 | private static class FrameHeader {
73 | public int mOpcode;
74 | public boolean mFin;
75 | @SuppressWarnings("unused")
76 | public int mReserved;
77 | public int mHeaderLen;
78 | public int mPayloadLen;
79 | public int mTotalLen;
80 | public byte[] mMask;
81 | }
82 |
83 |
84 | /**
85 | * Create new WebSockets background reader.
86 | *
87 | * @param master The message handler of master (foreground thread).
88 | * @param socket The socket channel created on foreground thread.
89 | */
90 | public WebSocketReader(Handler master, SocketChannel socket, WebSocketOptions options, String threadName) {
91 |
92 | super(threadName);
93 |
94 | mMaster = master;
95 | mSocket = socket;
96 | mOptions = options;
97 |
98 | mFrameBuffer = ByteBuffer.allocateDirect(options.getMaxFramePayloadSize() + 14);
99 | mMessagePayload = new NoCopyByteArrayOutputStream(options.getMaxMessagePayloadSize());
100 |
101 | mFrameHeader = null;
102 | mState = STATE_CONNECTING;
103 |
104 | if (DEBUG) Log.d(TAG, "created");
105 | }
106 |
107 |
108 | /**
109 | * Graceful shutdown of background reader thread (called from master).
110 | */
111 | public void quit() {
112 |
113 | mStopped = true;
114 |
115 | if (DEBUG) Log.d(TAG, "quit");
116 | }
117 |
118 |
119 | /**
120 | * Notify the master (foreground thread) of WebSockets message received
121 | * and unwrapped.
122 | *
123 | * @param message Message to send to master.
124 | */
125 | protected void notify(Object message) {
126 |
127 | Message msg = mMaster.obtainMessage();
128 | msg.obj = message;
129 | mMaster.sendMessage(msg);
130 | }
131 |
132 |
133 | /**
134 | * Process incoming WebSockets data (after handshake).
135 | */
136 | private boolean processData() throws Exception {
137 |
138 | // outside frame?
139 | if (mFrameHeader == null) {
140 |
141 | // need at least 2 bytes from WS frame header to start processing
142 | if (mFrameBuffer.position() >= 2) {
143 |
144 | byte b0 = mFrameBuffer.get(0);
145 | boolean fin = (b0 & 0x80) != 0;
146 | int rsv = (b0 & 0x70) >> 4;
147 | int opcode = b0 & 0x0f;
148 |
149 | byte b1 = mFrameBuffer.get(1);
150 | boolean masked = (b1 & 0x80) != 0;
151 | int payload_len1 = b1 & 0x7f;
152 |
153 | // now check protocol compliance
154 |
155 | if (rsv != 0) {
156 | throw new WebSocketException("RSV != 0 and no extension negotiated");
157 | }
158 |
159 | if (masked) {
160 | // currently, we don't allow this. need to see whats the final spec.
161 | throw new WebSocketException("masked server frame");
162 | }
163 |
164 | if (opcode > 7) {
165 | // control frame
166 | if (!fin) {
167 | throw new WebSocketException("fragmented control frame");
168 | }
169 | if (payload_len1 > 125) {
170 | throw new WebSocketException("control frame with payload length > 125 octets");
171 | }
172 | if (opcode != 8 && opcode != 9 && opcode != 10) {
173 | throw new WebSocketException("control frame using reserved opcode " + opcode);
174 | }
175 | if (opcode == 8 && payload_len1 == 1) {
176 | throw new WebSocketException("received close control frame with payload len 1");
177 | }
178 | } else {
179 | // message frame
180 | if (opcode != 0 && opcode != 1 && opcode != 2) {
181 | throw new WebSocketException("data frame using reserved opcode " + opcode);
182 | }
183 | if (!mInsideMessage && opcode == 0) {
184 | throw new WebSocketException("received continuation data frame outside fragmented message");
185 | }
186 | if (mInsideMessage && opcode != 0) {
187 | throw new WebSocketException("received non-continuation data frame while inside fragmented message");
188 | }
189 | }
190 |
191 | int mask_len = masked ? 4 : 0;
192 | int header_len = 0;
193 |
194 | if (payload_len1 < 126) {
195 | header_len = 2 + mask_len;
196 | } else if (payload_len1 == 126) {
197 | header_len = 2 + 2 + mask_len;
198 | } else if (payload_len1 == 127) {
199 | header_len = 2 + 8 + mask_len;
200 | } else {
201 | // should not arrive here
202 | throw new Exception("logic error");
203 | }
204 |
205 | // continue when complete frame header is available
206 | if (mFrameBuffer.position() >= header_len) {
207 |
208 | // determine frame payload length
209 | int i = 2;
210 | long payload_len = 0;
211 | if (payload_len1 == 126) {
212 | payload_len = ((0xff & mFrameBuffer.get(i)) << 8) | (0xff & mFrameBuffer.get(i+1));
213 | if (payload_len < 126) {
214 | throw new WebSocketException("invalid data frame length (not using minimal length encoding)");
215 | }
216 | i += 2;
217 | } else if (payload_len1 == 127) {
218 | if ((0x80 & mFrameBuffer.get(i+0)) != 0) {
219 | throw new WebSocketException("invalid data frame length (> 2^63)");
220 | }
221 | payload_len = ((0xff & mFrameBuffer.get(i+0)) << 56) |
222 | ((0xff & mFrameBuffer.get(i+1)) << 48) |
223 | ((0xff & mFrameBuffer.get(i+2)) << 40) |
224 | ((0xff & mFrameBuffer.get(i+3)) << 32) |
225 | ((0xff & mFrameBuffer.get(i+4)) << 24) |
226 | ((0xff & mFrameBuffer.get(i+5)) << 16) |
227 | ((0xff & mFrameBuffer.get(i+6)) << 8) |
228 | ((0xff & mFrameBuffer.get(i+7)) );
229 | if (payload_len < 65536) {
230 | throw new WebSocketException("invalid data frame length (not using minimal length encoding)");
231 | }
232 | i += 8;
233 | } else {
234 | payload_len = payload_len1;
235 | }
236 |
237 | // immediately bail out on frame too large
238 | if (payload_len > mOptions.getMaxFramePayloadSize()) {
239 | throw new WebSocketException("frame payload too large");
240 | }
241 |
242 | // save frame header metadata
243 | mFrameHeader = new FrameHeader();
244 | mFrameHeader.mOpcode = opcode;
245 | mFrameHeader.mFin = fin;
246 | mFrameHeader.mReserved = rsv;
247 | mFrameHeader.mPayloadLen = (int) payload_len;
248 | mFrameHeader.mHeaderLen = header_len;
249 | mFrameHeader.mTotalLen = mFrameHeader.mHeaderLen + mFrameHeader.mPayloadLen;
250 | if (masked) {
251 | mFrameHeader.mMask = new byte[4];
252 | for (int j = 0; j < 4; ++j) {
253 | mFrameHeader.mMask[i] = (byte) (0xff & mFrameBuffer.get(i + j));
254 | }
255 | i += 4;
256 | } else {
257 | mFrameHeader.mMask = null;
258 | }
259 |
260 | // continue processing when payload empty or completely buffered
261 | return mFrameHeader.mPayloadLen == 0 || mFrameBuffer.position() >= mFrameHeader.mTotalLen;
262 |
263 | } else {
264 |
265 | // need more data
266 | return false;
267 | }
268 | } else {
269 |
270 | // need more data
271 | return false;
272 | }
273 |
274 | } else {
275 |
276 | /// \todo refactor this for streaming processing, incl. fail fast on invalid UTF-8 within frame already
277 |
278 | // within frame
279 |
280 | // see if we buffered complete frame
281 | if (mFrameBuffer.position() >= mFrameHeader.mTotalLen) {
282 |
283 | // cut out frame payload
284 | byte[] framePayload = null;
285 | int oldPosition = mFrameBuffer.position();
286 | if (mFrameHeader.mPayloadLen > 0) {
287 | framePayload = new byte[mFrameHeader.mPayloadLen];
288 | mFrameBuffer.position(mFrameHeader.mHeaderLen);
289 | mFrameBuffer.get(framePayload, 0, (int) mFrameHeader.mPayloadLen);
290 | }
291 | mFrameBuffer.position(mFrameHeader.mTotalLen);
292 | mFrameBuffer.limit(oldPosition);
293 | mFrameBuffer.compact();
294 |
295 | if (mFrameHeader.mOpcode > 7) {
296 | // control frame
297 |
298 | if (mFrameHeader.mOpcode == 8) {
299 |
300 | int code = 1005; // CLOSE_STATUS_CODE_NULL : no status code received
301 | String reason = null;
302 |
303 | if (mFrameHeader.mPayloadLen >= 2) {
304 |
305 | // parse and check close code
306 | code = (framePayload[0] & 0xff) * 256 + (framePayload[1] & 0xff);
307 | if (code < 1000
308 | || (code >= 1000 && code <= 2999 &&
309 | code != 1000 && code != 1001 && code != 1002 && code != 1003 && code != 1007 && code != 1008 && code != 1009 && code != 1010 && code != 1011)
310 | || code >= 5000) {
311 |
312 | throw new WebSocketException("invalid close code " + code);
313 | }
314 |
315 | // parse and check close reason
316 | if (mFrameHeader.mPayloadLen > 2) {
317 |
318 | byte[] ra = new byte[mFrameHeader.mPayloadLen - 2];
319 | System.arraycopy(framePayload, 2, ra, 0, mFrameHeader.mPayloadLen - 2);
320 |
321 | Utf8Validator val = new Utf8Validator();
322 | val.validate(ra);
323 | if (!val.isValid()) {
324 | throw new WebSocketException("invalid close reasons (not UTF-8)");
325 | } else {
326 | reason = new String(ra, "UTF-8");
327 | }
328 | }
329 | }
330 | onClose(code, reason);
331 |
332 | } else if (mFrameHeader.mOpcode == 9) {
333 | // dispatch WS ping
334 | onPing(framePayload);
335 |
336 | } else if (mFrameHeader.mOpcode == 10) {
337 | // dispatch WS pong
338 | onPong(framePayload);
339 |
340 | } else {
341 |
342 | // should not arrive here (handled before)
343 | throw new Exception("logic error");
344 | }
345 |
346 | } else {
347 | // message frame
348 |
349 | if (!mInsideMessage) {
350 | // new message started
351 | mInsideMessage = true;
352 | mMessageOpcode = mFrameHeader.mOpcode;
353 | if (mMessageOpcode == 1 && mOptions.getValidateIncomingUtf8()) {
354 | mUtf8Validator.reset();
355 | }
356 | }
357 |
358 | if (framePayload != null) {
359 |
360 | // immediately bail out on message too large
361 | if (mMessagePayload.size() + framePayload.length > mOptions.getMaxMessagePayloadSize()) {
362 | throw new WebSocketException("message payload too large");
363 | }
364 |
365 | // validate incoming UTF-8
366 | if (mMessageOpcode == 1 && mOptions.getValidateIncomingUtf8() && !mUtf8Validator.validate(framePayload)) {
367 | throw new WebSocketException("invalid UTF-8 in text message payload");
368 | }
369 |
370 | // buffer frame payload for message
371 | mMessagePayload.write(framePayload);
372 | }
373 |
374 | // on final frame ..
375 | if (mFrameHeader.mFin) {
376 |
377 | if (mMessageOpcode == 1) {
378 |
379 | // verify that UTF-8 ends on codepoint
380 | if (mOptions.getValidateIncomingUtf8() && !mUtf8Validator.isValid()) {
381 | throw new WebSocketException("UTF-8 text message payload ended within Unicode code point");
382 | }
383 |
384 | // deliver text message
385 | if (mOptions.getReceiveTextMessagesRaw()) {
386 |
387 | // dispatch WS text message as raw (but validated) UTF-8
388 | onRawTextMessage(mMessagePayload.toByteArray());
389 |
390 | } else {
391 |
392 | // dispatch WS text message as Java String (previously already validated)
393 | String s = new String(mMessagePayload.toByteArray(), "UTF-8");
394 | onTextMessage(s);
395 | }
396 |
397 | } else if (mMessageOpcode == 2) {
398 |
399 | // dispatch WS binary message
400 | onBinaryMessage(mMessagePayload.toByteArray());
401 |
402 | } else {
403 |
404 | // should not arrive here (handled before)
405 | throw new Exception("logic error");
406 | }
407 |
408 | // ok, message completed - reset all
409 | mInsideMessage = false;
410 | mMessagePayload.reset();
411 | }
412 | }
413 |
414 | // reset frame
415 | mFrameHeader = null;
416 |
417 | // reprocess if more data left
418 | return mFrameBuffer.position() > 0;
419 |
420 | } else {
421 |
422 | // need more data
423 | return false;
424 | }
425 | }
426 | }
427 |
428 |
429 | /**
430 | * WebSockets handshake reply from server received, default notifies master.
431 | *
432 | * @param success Success handshake flag
433 | */
434 | protected void onHandshake(boolean success) {
435 |
436 | notify(new WebSocketMessage.ServerHandshake(success));
437 | }
438 |
439 |
440 | /**
441 | * WebSockets close received, default notifies master.
442 | */
443 | protected void onClose(int code, String reason) {
444 |
445 | notify(new WebSocketMessage.Close(code, reason));
446 | }
447 |
448 |
449 | /**
450 | * WebSockets ping received, default notifies master.
451 | *
452 | * @param payload Ping payload or null.
453 | */
454 | protected void onPing(byte[] payload) {
455 |
456 | notify(new WebSocketMessage.Ping(payload));
457 | }
458 |
459 |
460 | /**
461 | * WebSockets pong received, default notifies master.
462 | *
463 | * @param payload Pong payload or null.
464 | */
465 | protected void onPong(byte[] payload) {
466 |
467 | notify(new WebSocketMessage.Pong(payload));
468 | }
469 |
470 |
471 | /**
472 | * WebSockets text message received, default notifies master.
473 | * This will only be called when the option receiveTextMessagesRaw
474 | * HAS NOT been set.
475 | *
476 | * @param payload Text message payload as Java String decoded
477 | * from raw UTF-8 payload or null (empty payload).
478 | */
479 | protected void onTextMessage(String payload) {
480 |
481 | notify(new WebSocketMessage.TextMessage(payload));
482 | }
483 |
484 |
485 | /**
486 | * WebSockets text message received, default notifies master.
487 | * This will only be called when the option receiveTextMessagesRaw
488 | * HAS been set.
489 | *
490 | * @param payload Text message payload as raw UTF-8 octets or
491 | * null (empty payload).
492 | */
493 | protected void onRawTextMessage(byte[] payload) {
494 |
495 | notify(new WebSocketMessage.RawTextMessage(payload));
496 | }
497 |
498 |
499 | /**
500 | * WebSockets binary message received, default notifies master.
501 | *
502 | * @param payload Binary message payload or null (empty payload).
503 | */
504 | protected void onBinaryMessage(byte[] payload) {
505 |
506 | notify(new WebSocketMessage.BinaryMessage(payload));
507 | }
508 |
509 |
510 | /**
511 | * Process WebSockets handshake received from server.
512 | */
513 | private boolean processHandshake() throws UnsupportedEncodingException {
514 |
515 | boolean res = false;
516 | for (int pos = mFrameBuffer.position() - 4; pos >= 0; --pos) {
517 | if (mFrameBuffer.get(pos+0) == 0x0d &&
518 | mFrameBuffer.get(pos+1) == 0x0a &&
519 | mFrameBuffer.get(pos+2) == 0x0d &&
520 | mFrameBuffer.get(pos+3) == 0x0a) {
521 |
522 | /// \todo process & verify handshake from server
523 | /// \todo forward subprotocol, if any
524 |
525 | int oldPosition = mFrameBuffer.position();
526 |
527 | // Check HTTP status code
528 | boolean serverError = false;
529 | if (mFrameBuffer.get(0) == 'H' &&
530 | mFrameBuffer.get(1) == 'T' &&
531 | mFrameBuffer.get(2) == 'T' &&
532 | mFrameBuffer.get(3) == 'P') {
533 |
534 | Pair status = parseHttpStatus();
535 | if (status.first >= 300) {
536 | // Invalid status code for success connection
537 | notify(new WebSocketMessage.ServerError(status.first, status.second));
538 | serverError = true;
539 | }
540 | }
541 |
542 | mFrameBuffer.position(pos + 4);
543 | mFrameBuffer.limit(oldPosition);
544 | mFrameBuffer.compact();
545 |
546 | if (!serverError) {
547 | // process further when data after HTTP headers left in buffer
548 | res = mFrameBuffer.position() > 0;
549 |
550 | mState = STATE_OPEN;
551 | } else {
552 | res = true;
553 | mState = STATE_CLOSED;
554 | mStopped = true;
555 | }
556 |
557 | onHandshake(!serverError);
558 | break;
559 | }
560 | }
561 | return res;
562 | }
563 |
564 | @SuppressWarnings("unused")
565 | private Map parseHttpHeaders(byte[] buffer) throws UnsupportedEncodingException {
566 | // TODO: use utf-8 validator?
567 | String s = new String(buffer, "UTF-8");
568 | Map headers = new HashMap();
569 |
570 | String[] lines = s.split("\r\n");
571 | for (String line : lines) {
572 | if (line.length() > 0) {
573 | String[] h = line.split(": ");
574 | if (h.length == 2) {
575 | headers.put(h[0], h[1]);
576 | Log.w(TAG, String.format("'%s'='%s'", h[0], h[1]));
577 | }
578 | }
579 | }
580 |
581 | return headers;
582 | }
583 |
584 | private Pair parseHttpStatus() throws UnsupportedEncodingException {
585 | int beg, end;
586 | // Find first space
587 | for (beg = 4; beg < mFrameBuffer.position(); ++beg) {
588 | if (mFrameBuffer.get(beg) == ' ') break;
589 | }
590 | // Find second space
591 | for (end = beg + 1; end < mFrameBuffer.position(); ++end) {
592 | if (mFrameBuffer.get(end) == ' ') break;
593 | }
594 | // Parse status code between them
595 | ++beg;
596 | int statusCode = 0;
597 | for (int i = 0; beg + i < end; ++i) {
598 | int digit = (mFrameBuffer.get(beg + i) - 0x30);
599 | statusCode *= 10;
600 | statusCode += digit;
601 | }
602 | // Find end of line to extract error message
603 | ++end;
604 | int eol;
605 | for (eol = end; eol < mFrameBuffer.position(); ++eol) {
606 | if (mFrameBuffer.get(eol) == 0x0d) break;
607 | }
608 | int statusMessageLength = eol - end;
609 | byte[] statusBuf = new byte[statusMessageLength];
610 | mFrameBuffer.position(end);
611 | mFrameBuffer.get(statusBuf, 0, statusMessageLength);
612 | String statusMessage = new String(statusBuf, "UTF-8");
613 | if (DEBUG) Log.w(TAG, String.format("Status: %d (%s)", statusCode, statusMessage));
614 | return new Pair(statusCode, statusMessage);
615 | }
616 |
617 |
618 | /**
619 | * Consume data buffered in mFrameBuffer.
620 | */
621 | private boolean consumeData() throws Exception {
622 |
623 | if (mState == STATE_OPEN || mState == STATE_CLOSING) {
624 |
625 | return processData();
626 |
627 | } else if (mState == STATE_CONNECTING) {
628 |
629 | return processHandshake();
630 |
631 | } else if (mState == STATE_CLOSED) {
632 |
633 | return false;
634 |
635 | } else {
636 | // should not arrive here
637 | return false;
638 | }
639 |
640 | }
641 |
642 |
643 | /**
644 | * Run the background reader thread loop.
645 | */
646 | @Override
647 | public void run() {
648 |
649 | if (DEBUG) Log.d(TAG, "running");
650 |
651 | try {
652 |
653 | mFrameBuffer.clear();
654 | do {
655 | // blocking read on socket
656 | int len = mSocket.read(mFrameBuffer);
657 | if (len > 0) {
658 | // process buffered data
659 | while (consumeData()) {
660 | }
661 | } else if (len < 0) {
662 |
663 | if (DEBUG) Log.d(TAG, "run() : ConnectionLost");
664 |
665 | notify(new WebSocketMessage.ConnectionLost());
666 | mStopped = true;
667 | }
668 | } while (!mStopped);
669 |
670 | } catch (WebSocketException e) {
671 |
672 | if (DEBUG) Log.d(TAG, "run() : WebSocketException (" + e.toString() + ")");
673 |
674 | // wrap the exception and notify master
675 | notify(new WebSocketMessage.ProtocolViolation(e));
676 |
677 | } catch (SocketException e) {
678 |
679 | if (DEBUG) Log.d(TAG, "run() : SocketException (" + e.toString() + ")");
680 |
681 | // wrap the exception and notify master
682 | notify(new WebSocketMessage.ConnectionLost());;
683 |
684 | } catch (Exception e) {
685 |
686 | if (DEBUG) Log.d(TAG, "run() : Exception (" + e.toString() + ")");
687 |
688 | // wrap the exception and notify master
689 | notify(new WebSocketMessage.Error(e));
690 |
691 | } finally {
692 |
693 | mStopped = true;
694 | }
695 |
696 | if (DEBUG) Log.d(TAG, "ended");
697 | }
698 | }
699 |
--------------------------------------------------------------------------------
/src/de/tavendo/autobahn/WebSocketWriter.java:
--------------------------------------------------------------------------------
1 | /******************************************************************************
2 | *
3 | * Copyright 2011-2012 Tavendo GmbH
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | ******************************************************************************/
18 |
19 | package de.tavendo.autobahn;
20 |
21 | import java.io.IOException;
22 | import java.net.SocketException;
23 | import java.nio.channels.SocketChannel;
24 | import java.util.Random;
25 |
26 | import android.os.Handler;
27 | import android.os.Looper;
28 | import android.os.Message;
29 | import android.util.Base64;
30 | import android.util.Log;
31 |
32 | /**
33 | * WebSocket writer, the sending leg of a WebSockets connection.
34 | * This is run on it's background thread with it's own message loop.
35 | * The only method that needs to be called (from foreground thread) is forward(),
36 | * which is used to forward a WebSockets message to this object (running on
37 | * background thread) so that it can be formatted and sent out on the
38 | * underlying TCP socket.
39 | */
40 | public class WebSocketWriter extends Handler {
41 |
42 | private static final boolean DEBUG = true;
43 | private static final String TAG = WebSocketWriter.class.getName();
44 |
45 | /// Random number generator for handshake key and frame mask generation.
46 | private final Random mRng = new Random();
47 |
48 | /// Connection master.
49 | private final Handler mMaster;
50 |
51 | /// Message looper this object is running on.
52 | private final Looper mLooper;
53 |
54 | /// The NIO socket channel created on foreground thread.
55 | private final SocketChannel mSocket;
56 |
57 | /// WebSockets options.
58 | private final WebSocketOptions mOptions;
59 |
60 | /// The send buffer that holds data to send on socket.
61 | private final ByteBufferOutputStream mBuffer;
62 |
63 |
64 | /**
65 | * Create new WebSockets background writer.
66 | *
67 | * @param looper The message looper of the background thread on which
68 | * this object is running.
69 | * @param master The message handler of master (foreground thread).
70 | * @param socket The socket channel created on foreground thread.
71 | * @param options WebSockets connection options.
72 | */
73 | public WebSocketWriter(Looper looper, Handler master, SocketChannel socket, WebSocketOptions options) {
74 |
75 | super(looper);
76 |
77 | mLooper = looper;
78 | mMaster = master;
79 | mSocket = socket;
80 | mOptions = options;
81 | mBuffer = new ByteBufferOutputStream(options.getMaxFramePayloadSize() + 14, 4*64*1024);
82 |
83 | if (DEBUG) Log.d(TAG, "created");
84 | }
85 |
86 |
87 | /**
88 | * Call this from the foreground (UI) thread to make the writer
89 | * (running on background thread) send a WebSocket message on the
90 | * underlying TCP.
91 | *
92 | * @param message Message to send to WebSockets writer. An instance of the message
93 | * classes inside WebSocketMessage or another type which then needs
94 | * to be handled within processAppMessage() (in a class derived from
95 | * this class).
96 | */
97 | public void forward(Object message) {
98 |
99 | Message msg = obtainMessage();
100 | msg.obj = message;
101 | sendMessage(msg);
102 | }
103 |
104 |
105 | /**
106 | * Notify the master (foreground thread).
107 | *
108 | * @param message Message to send to master.
109 | */
110 | private void notify(Object message) {
111 |
112 | Message msg = mMaster.obtainMessage();
113 | msg.obj = message;
114 | mMaster.sendMessage(msg);
115 | }
116 |
117 |
118 | /**
119 | * Create new key for WebSockets handshake.
120 | *
121 | * @return WebSockets handshake key (Base64 encoded).
122 | */
123 | private String newHandshakeKey() {
124 | final byte[] ba = new byte[16];
125 | mRng.nextBytes(ba);
126 | return Base64.encodeToString(ba, Base64.NO_WRAP);
127 | }
128 |
129 |
130 | /**
131 | * Create new (random) frame mask.
132 | *
133 | * @return Frame mask (4 octets).
134 | */
135 | private byte[] newFrameMask() {
136 | final byte[] ba = new byte[4];
137 | mRng.nextBytes(ba);
138 | return ba;
139 | }
140 |
141 |
142 | /**
143 | * Send WebSocket client handshake.
144 | */
145 | private void sendClientHandshake(WebSocketMessage.ClientHandshake message) throws IOException {
146 |
147 | // write HTTP header with handshake
148 | String path;
149 | if (message.mQuery != null) {
150 | path = message.mPath + "?" + message.mQuery;
151 | } else {
152 | path = message.mPath;
153 | }
154 | mBuffer.write("GET " + path + " HTTP/1.1");
155 | mBuffer.crlf();
156 | mBuffer.write("Host: " + message.mHost);
157 | mBuffer.crlf();
158 | mBuffer.write("Upgrade: WebSocket");
159 | mBuffer.crlf();
160 | mBuffer.write("Connection: Upgrade");
161 | mBuffer.crlf();
162 |
163 | mBuffer.write("Sec-WebSocket-Key: " + newHandshakeKey());
164 | mBuffer.crlf();
165 |
166 | if (message.mOrigin != null && !message.mOrigin.equals("")) {
167 | mBuffer.write("Origin: " + message.mOrigin);
168 | mBuffer.crlf();
169 | }
170 |
171 | if (message.mSubprotocols != null && message.mSubprotocols.length() > 0) {
172 | mBuffer.write("Sec-WebSocket-Protocol: ");
173 | mBuffer.write(message.mSubprotocols);
174 | mBuffer.crlf();
175 | }
176 |
177 | mBuffer.write("Sec-WebSocket-Version: 13");
178 | mBuffer.crlf();
179 |
180 | mBuffer.crlf();
181 | }
182 |
183 |
184 | /**
185 | * Send WebSockets close.
186 | */
187 | private void sendClose(WebSocketMessage.Close message) throws IOException, WebSocketException {
188 |
189 | if (message.mCode > 0) {
190 |
191 | byte[] payload = null;
192 |
193 | if (message.mReason != null && !message.mReason.equals("")) {
194 | byte[] pReason = message.mReason.getBytes("UTF-8");
195 | payload = new byte[2 + pReason.length];
196 | for (int i = 0; i < pReason.length; ++i) {
197 | payload[i + 2] = pReason[i];
198 | }
199 | } else {
200 | payload = new byte[2];
201 | }
202 |
203 | if (payload != null && payload.length > 125) {
204 | throw new WebSocketException("close payload exceeds 125 octets");
205 | }
206 |
207 | payload[0] = (byte)((message.mCode >> 8) & 0xff);
208 | payload[1] = (byte)(message.mCode & 0xff);
209 |
210 | sendFrame(8, true, payload);
211 |
212 | } else {
213 |
214 | sendFrame(8, true, null);
215 | }
216 | }
217 |
218 |
219 | /**
220 | * Send WebSockets ping.
221 | */
222 | private void sendPing(WebSocketMessage.Ping message) throws IOException, WebSocketException {
223 | if (message.mPayload != null && message.mPayload.length > 125) {
224 | throw new WebSocketException("ping payload exceeds 125 octets");
225 | }
226 | sendFrame(9, true, message.mPayload);
227 | }
228 |
229 |
230 | /**
231 | * Send WebSockets pong. Normally, unsolicited Pongs are not used,
232 | * but Pongs are only send in response to a Ping from the peer.
233 | */
234 | private void sendPong(WebSocketMessage.Pong message) throws IOException, WebSocketException {
235 | if (message.mPayload != null && message.mPayload.length > 125) {
236 | throw new WebSocketException("pong payload exceeds 125 octets");
237 | }
238 | sendFrame(10, true, message.mPayload);
239 | }
240 |
241 |
242 | /**
243 | * Send WebSockets binary message.
244 | */
245 | private void sendBinaryMessage(WebSocketMessage.BinaryMessage message) throws IOException, WebSocketException {
246 | if (message.mPayload.length > mOptions.getMaxMessagePayloadSize()) {
247 | throw new WebSocketException("message payload exceeds payload limit");
248 | }
249 | sendFrame(2, true, message.mPayload);
250 | }
251 |
252 |
253 | /**
254 | * Send WebSockets text message.
255 | */
256 | private void sendTextMessage(WebSocketMessage.TextMessage message) throws IOException, WebSocketException {
257 | byte[] payload = message.mPayload.getBytes("UTF-8");
258 | if (payload.length > mOptions.getMaxMessagePayloadSize()) {
259 | throw new WebSocketException("message payload exceeds payload limit");
260 | }
261 | sendFrame(1, true, payload);
262 | }
263 |
264 |
265 | /**
266 | * Send WebSockets binary message.
267 | */
268 | private void sendRawTextMessage(WebSocketMessage.RawTextMessage message) throws IOException, WebSocketException {
269 | if (message.mPayload.length > mOptions.getMaxMessagePayloadSize()) {
270 | throw new WebSocketException("message payload exceeds payload limit");
271 | }
272 | sendFrame(1, true, message.mPayload);
273 | }
274 |
275 |
276 | /**
277 | * Sends a WebSockets frame. Only need to use this method in derived classes which implement
278 | * more message types in processAppMessage(). You need to know what you are doing!
279 | *
280 | * @param opcode The WebSocket frame opcode.
281 | * @param fin FIN flag for WebSocket frame.
282 | * @param payload Frame payload or null.
283 | */
284 | protected void sendFrame(int opcode, boolean fin, byte[] payload) throws IOException {
285 | if (payload != null) {
286 | sendFrame(opcode, fin, payload, 0, payload.length);
287 | } else {
288 | sendFrame(opcode, fin, null, 0, 0);
289 | }
290 | }
291 |
292 |
293 | /**
294 | * Sends a WebSockets frame. Only need to use this method in derived classes which implement
295 | * more message types in processAppMessage(). You need to know what you are doing!
296 | *
297 | * @param opcode The WebSocket frame opcode.
298 | * @param fin FIN flag for WebSocket frame.
299 | * @param payload Frame payload or null.
300 | * @param offset Offset within payload of the chunk to send.
301 | * @param length Length of the chunk within payload to send.
302 | */
303 | protected void sendFrame(int opcode, boolean fin, byte[] payload, int offset, int length) throws IOException {
304 |
305 | // first octet
306 | byte b0 = 0;
307 | if (fin) {
308 | b0 |= (byte) (1 << 7);
309 | }
310 | b0 |= (byte) opcode;
311 | mBuffer.write(b0);
312 |
313 | // second octet
314 | byte b1 = 0;
315 | if (mOptions.getMaskClientFrames()) {
316 | b1 = (byte) (1 << 7);
317 | }
318 |
319 | long len = length;
320 |
321 | // extended payload length
322 | if (len <= 125) {
323 | b1 |= (byte) len;
324 | mBuffer.write(b1);
325 | } else if (len <= 0xffff) {
326 | b1 |= (byte) (126 & 0xff);
327 | mBuffer.write(b1);
328 | mBuffer.write(new byte[] {(byte)((len >> 8) & 0xff),
329 | (byte)(len & 0xff)});
330 | } else {
331 | b1 |= (byte) (127 & 0xff);
332 | mBuffer.write(b1);
333 | mBuffer.write(new byte[] {(byte)((len >> 56) & 0xff),
334 | (byte)((len >> 48) & 0xff),
335 | (byte)((len >> 40) & 0xff),
336 | (byte)((len >> 32) & 0xff),
337 | (byte)((len >> 24) & 0xff),
338 | (byte)((len >> 16) & 0xff),
339 | (byte)((len >> 8) & 0xff),
340 | (byte)(len & 0xff)});
341 | }
342 |
343 | byte mask[] = null;
344 | if (mOptions.getMaskClientFrames()) {
345 | // a mask is always needed, even without payload
346 | mask = newFrameMask();
347 | mBuffer.write(mask[0]);
348 | mBuffer.write(mask[1]);
349 | mBuffer.write(mask[2]);
350 | mBuffer.write(mask[3]);
351 | }
352 |
353 | if (len > 0) {
354 | if (mOptions.getMaskClientFrames()) {
355 | /// \todo optimize masking
356 | /// \todo masking within buffer of output stream
357 | for (int i = 0; i < len; ++i) {
358 | payload[i + offset] ^= mask[i % 4];
359 | }
360 | }
361 | mBuffer.write(payload, offset, length);
362 | }
363 | }
364 |
365 |
366 | /**
367 | * Process message received from foreground thread. This is called from
368 | * the message looper set up for the background thread running this writer.
369 | *
370 | * @param msg Message from thread message queue.
371 | */
372 | @Override
373 | public void handleMessage(Message msg) {
374 |
375 | try {
376 |
377 | // clear send buffer
378 | mBuffer.clear();
379 |
380 | // process message from master
381 | processMessage(msg.obj);
382 |
383 | // send out buffered data
384 | mBuffer.flip();
385 | while (mBuffer.remaining() > 0) {
386 | // this can block on socket write
387 | @SuppressWarnings("unused")
388 | int written = mSocket.write(mBuffer.getBuffer());
389 | }
390 |
391 | } catch (SocketException e) {
392 |
393 | if (DEBUG) Log.d(TAG, "run() : SocketException (" + e.toString() + ")");
394 |
395 | // wrap the exception and notify master
396 | notify(new WebSocketMessage.ConnectionLost());
397 | } catch (Exception e) {
398 |
399 | if (DEBUG) e.printStackTrace();
400 |
401 | // wrap the exception and notify master
402 | notify(new WebSocketMessage.Error(e));
403 | }
404 | }
405 |
406 |
407 | /**
408 | * Process WebSockets or control message from master. Normally,
409 | * there should be no reason to override this. If you do, you
410 | * need to know what you are doing.
411 | *
412 | * @param msg An instance of the message types within WebSocketMessage
413 | * or a message that is handled in processAppMessage().
414 | */
415 | protected void processMessage(Object msg) throws IOException, WebSocketException {
416 |
417 | if (msg instanceof WebSocketMessage.TextMessage) {
418 |
419 | sendTextMessage((WebSocketMessage.TextMessage) msg);
420 |
421 | } else if (msg instanceof WebSocketMessage.RawTextMessage) {
422 |
423 | sendRawTextMessage((WebSocketMessage.RawTextMessage) msg);
424 |
425 | } else if (msg instanceof WebSocketMessage.BinaryMessage) {
426 |
427 | sendBinaryMessage((WebSocketMessage.BinaryMessage) msg);
428 |
429 | } else if (msg instanceof WebSocketMessage.Ping) {
430 |
431 | sendPing((WebSocketMessage.Ping) msg);
432 |
433 | } else if (msg instanceof WebSocketMessage.Pong) {
434 |
435 | sendPong((WebSocketMessage.Pong) msg);
436 |
437 | } else if (msg instanceof WebSocketMessage.Close) {
438 |
439 | sendClose((WebSocketMessage.Close) msg);
440 |
441 | } else if (msg instanceof WebSocketMessage.ClientHandshake) {
442 |
443 | sendClientHandshake((WebSocketMessage.ClientHandshake) msg);
444 |
445 | } else if (msg instanceof WebSocketMessage.Quit) {
446 |
447 | mLooper.quit();
448 |
449 | if (DEBUG) Log.d(TAG, "ended");
450 |
451 | return;
452 |
453 | } else {
454 |
455 | // call hook which may be overridden in derived class to process
456 | // messages we don't understand in this class
457 | processAppMessage(msg);
458 | }
459 | }
460 |
461 |
462 | /**
463 | * Process message other than plain WebSockets or control message.
464 | * This is intended to be overridden in derived classes.
465 | *
466 | * @param msg Message from foreground thread to process.
467 | */
468 | protected void processAppMessage(Object msg) throws WebSocketException, IOException {
469 |
470 | throw new WebSocketException("unknown message received by WebSocketWriter");
471 | }
472 | }
473 |
--------------------------------------------------------------------------------