OLD | NEW |
| (Empty) |
1 /* | |
2 * libjingle | |
3 * Copyright 2014 Google Inc. | |
4 * | |
5 * Redistribution and use in source and binary forms, with or without | |
6 * modification, are permitted provided that the following conditions are met: | |
7 * | |
8 * 1. Redistributions of source code must retain the above copyright notice, | |
9 * this list of conditions and the following disclaimer. | |
10 * 2. Redistributions in binary form must reproduce the above copyright notice, | |
11 * this list of conditions and the following disclaimer in the documentation | |
12 * and/or other materials provided with the distribution. | |
13 * 3. The name of the author may not be used to endorse or promote products | |
14 * derived from this software without specific prior written permission. | |
15 * | |
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED | |
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF | |
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO | |
19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; | |
22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | |
23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR | |
24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |
25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
26 */ | |
27 | |
28 package org.appspot.apprtc; | |
29 | |
30 import org.appspot.apprtc.util.AsyncHttpURLConnection; | |
31 import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; | |
32 import org.appspot.apprtc.util.LooperExecutor; | |
33 | |
34 import android.util.Log; | |
35 | |
36 import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver; | |
37 import de.tavendo.autobahn.WebSocketConnection; | |
38 import de.tavendo.autobahn.WebSocketException; | |
39 | |
40 import org.json.JSONException; | |
41 import org.json.JSONObject; | |
42 | |
43 import java.net.URI; | |
44 import java.net.URISyntaxException; | |
45 import java.util.LinkedList; | |
46 | |
47 /** | |
48 * WebSocket client implementation. | |
49 * | |
50 * <p>All public methods should be called from a looper executor thread | |
51 * passed in a constructor, otherwise exception will be thrown. | |
52 * All events are dispatched on the same thread. | |
53 */ | |
54 | |
55 public class WebSocketChannelClient { | |
56 private static final String TAG = "WSChannelRTCClient"; | |
57 private static final int CLOSE_TIMEOUT = 1000; | |
58 private final WebSocketChannelEvents events; | |
59 private final LooperExecutor executor; | |
60 private WebSocketConnection ws; | |
61 private WebSocketObserver wsObserver; | |
62 private String wsServerUrl; | |
63 private String postServerUrl; | |
64 private String roomID; | |
65 private String clientID; | |
66 private WebSocketConnectionState state; | |
67 private final Object closeEventLock = new Object(); | |
68 private boolean closeEvent; | |
69 // WebSocket send queue. Messages are added to the queue when WebSocket | |
70 // client is not registered and are consumed in register() call. | |
71 private final LinkedList<String> wsSendQueue; | |
72 | |
73 /** | |
74 * Possible WebSocket connection states. | |
75 */ | |
76 public enum WebSocketConnectionState { | |
77 NEW, CONNECTED, REGISTERED, CLOSED, ERROR | |
78 }; | |
79 | |
80 /** | |
81 * Callback interface for messages delivered on WebSocket. | |
82 * All events are dispatched from a looper executor thread. | |
83 */ | |
84 public interface WebSocketChannelEvents { | |
85 public void onWebSocketMessage(final String message); | |
86 public void onWebSocketClose(); | |
87 public void onWebSocketError(final String description); | |
88 } | |
89 | |
90 public WebSocketChannelClient(LooperExecutor executor, WebSocketChannelEvents
events) { | |
91 this.executor = executor; | |
92 this.events = events; | |
93 roomID = null; | |
94 clientID = null; | |
95 wsSendQueue = new LinkedList<String>(); | |
96 state = WebSocketConnectionState.NEW; | |
97 } | |
98 | |
99 public WebSocketConnectionState getState() { | |
100 return state; | |
101 } | |
102 | |
103 public void connect(final String wsUrl, final String postUrl) { | |
104 checkIfCalledOnValidThread(); | |
105 if (state != WebSocketConnectionState.NEW) { | |
106 Log.e(TAG, "WebSocket is already connected."); | |
107 return; | |
108 } | |
109 wsServerUrl = wsUrl; | |
110 postServerUrl = postUrl; | |
111 closeEvent = false; | |
112 | |
113 Log.d(TAG, "Connecting WebSocket to: " + wsUrl + ". Post URL: " + postUrl); | |
114 ws = new WebSocketConnection(); | |
115 wsObserver = new WebSocketObserver(); | |
116 try { | |
117 ws.connect(new URI(wsServerUrl), wsObserver); | |
118 } catch (URISyntaxException e) { | |
119 reportError("URI error: " + e.getMessage()); | |
120 } catch (WebSocketException e) { | |
121 reportError("WebSocket connection error: " + e.getMessage()); | |
122 } | |
123 } | |
124 | |
125 public void register(final String roomID, final String clientID) { | |
126 checkIfCalledOnValidThread(); | |
127 this.roomID = roomID; | |
128 this.clientID = clientID; | |
129 if (state != WebSocketConnectionState.CONNECTED) { | |
130 Log.w(TAG, "WebSocket register() in state " + state); | |
131 return; | |
132 } | |
133 Log.d(TAG, "Registering WebSocket for room " + roomID + ". CLientID: " + cli
entID); | |
134 JSONObject json = new JSONObject(); | |
135 try { | |
136 json.put("cmd", "register"); | |
137 json.put("roomid", roomID); | |
138 json.put("clientid", clientID); | |
139 Log.d(TAG, "C->WSS: " + json.toString()); | |
140 ws.sendTextMessage(json.toString()); | |
141 state = WebSocketConnectionState.REGISTERED; | |
142 // Send any previously accumulated messages. | |
143 for (String sendMessage : wsSendQueue) { | |
144 send(sendMessage); | |
145 } | |
146 wsSendQueue.clear(); | |
147 } catch (JSONException e) { | |
148 reportError("WebSocket register JSON error: " + e.getMessage()); | |
149 } | |
150 } | |
151 | |
152 public void send(String message) { | |
153 checkIfCalledOnValidThread(); | |
154 switch (state) { | |
155 case NEW: | |
156 case CONNECTED: | |
157 // Store outgoing messages and send them after websocket client | |
158 // is registered. | |
159 Log.d(TAG, "WS ACC: " + message); | |
160 wsSendQueue.add(message); | |
161 return; | |
162 case ERROR: | |
163 case CLOSED: | |
164 Log.e(TAG, "WebSocket send() in error or closed state : " + message); | |
165 return; | |
166 case REGISTERED: | |
167 JSONObject json = new JSONObject(); | |
168 try { | |
169 json.put("cmd", "send"); | |
170 json.put("msg", message); | |
171 message = json.toString(); | |
172 Log.d(TAG, "C->WSS: " + message); | |
173 ws.sendTextMessage(message); | |
174 } catch (JSONException e) { | |
175 reportError("WebSocket send JSON error: " + e.getMessage()); | |
176 } | |
177 break; | |
178 } | |
179 return; | |
180 } | |
181 | |
182 // This call can be used to send WebSocket messages before WebSocket | |
183 // connection is opened. | |
184 public void post(String message) { | |
185 checkIfCalledOnValidThread(); | |
186 sendWSSMessage("POST", message); | |
187 } | |
188 | |
189 public void disconnect(boolean waitForComplete) { | |
190 checkIfCalledOnValidThread(); | |
191 Log.d(TAG, "Disonnect WebSocket. State: " + state); | |
192 if (state == WebSocketConnectionState.REGISTERED) { | |
193 // Send "bye" to WebSocket server. | |
194 send("{\"type\": \"bye\"}"); | |
195 state = WebSocketConnectionState.CONNECTED; | |
196 // Send http DELETE to http WebSocket server. | |
197 sendWSSMessage("DELETE", ""); | |
198 } | |
199 // Close WebSocket in CONNECTED or ERROR states only. | |
200 if (state == WebSocketConnectionState.CONNECTED | |
201 || state == WebSocketConnectionState.ERROR) { | |
202 ws.disconnect(); | |
203 state = WebSocketConnectionState.CLOSED; | |
204 | |
205 // Wait for websocket close event to prevent websocket library from | |
206 // sending any pending messages to deleted looper thread. | |
207 if (waitForComplete) { | |
208 synchronized (closeEventLock) { | |
209 while (!closeEvent) { | |
210 try { | |
211 closeEventLock.wait(CLOSE_TIMEOUT); | |
212 break; | |
213 } catch (InterruptedException e) { | |
214 Log.e(TAG, "Wait error: " + e.toString()); | |
215 } | |
216 } | |
217 } | |
218 } | |
219 } | |
220 Log.d(TAG, "Disonnecting WebSocket done."); | |
221 } | |
222 | |
223 private void reportError(final String errorMessage) { | |
224 Log.e(TAG, errorMessage); | |
225 executor.execute(new Runnable() { | |
226 @Override | |
227 public void run() { | |
228 if (state != WebSocketConnectionState.ERROR) { | |
229 state = WebSocketConnectionState.ERROR; | |
230 events.onWebSocketError(errorMessage); | |
231 } | |
232 } | |
233 }); | |
234 } | |
235 | |
236 // Asynchronously send POST/DELETE to WebSocket server. | |
237 private void sendWSSMessage(final String method, final String message) { | |
238 String postUrl = postServerUrl + "/" + roomID + "/" + clientID; | |
239 Log.d(TAG, "WS " + method + " : " + postUrl + " : " + message); | |
240 AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection( | |
241 method, postUrl, message, new AsyncHttpEvents() { | |
242 @Override | |
243 public void onHttpError(String errorMessage) { | |
244 reportError("WS " + method + " error: " + errorMessage); | |
245 } | |
246 | |
247 @Override | |
248 public void onHttpComplete(String response) { | |
249 } | |
250 }); | |
251 httpConnection.send(); | |
252 } | |
253 | |
254 // Helper method for debugging purposes. Ensures that WebSocket method is | |
255 // called on a looper thread. | |
256 private void checkIfCalledOnValidThread() { | |
257 if (!executor.checkOnLooperThread()) { | |
258 throw new IllegalStateException( | |
259 "WebSocket method is not called on valid thread"); | |
260 } | |
261 } | |
262 | |
263 private class WebSocketObserver implements WebSocketConnectionObserver { | |
264 @Override | |
265 public void onOpen() { | |
266 Log.d(TAG, "WebSocket connection opened to: " + wsServerUrl); | |
267 executor.execute(new Runnable() { | |
268 @Override | |
269 public void run() { | |
270 state = WebSocketConnectionState.CONNECTED; | |
271 // Check if we have pending register request. | |
272 if (roomID != null && clientID != null) { | |
273 register(roomID, clientID); | |
274 } | |
275 } | |
276 }); | |
277 } | |
278 | |
279 @Override | |
280 public void onClose(WebSocketCloseNotification code, String reason) { | |
281 Log.d(TAG, "WebSocket connection closed. Code: " + code | |
282 + ". Reason: " + reason + ". State: " + state); | |
283 synchronized (closeEventLock) { | |
284 closeEvent = true; | |
285 closeEventLock.notify(); | |
286 } | |
287 executor.execute(new Runnable() { | |
288 @Override | |
289 public void run() { | |
290 if (state != WebSocketConnectionState.CLOSED) { | |
291 state = WebSocketConnectionState.CLOSED; | |
292 events.onWebSocketClose(); | |
293 } | |
294 } | |
295 }); | |
296 } | |
297 | |
298 @Override | |
299 public void onTextMessage(String payload) { | |
300 Log.d(TAG, "WSS->C: " + payload); | |
301 final String message = payload; | |
302 executor.execute(new Runnable() { | |
303 @Override | |
304 public void run() { | |
305 if (state == WebSocketConnectionState.CONNECTED | |
306 || state == WebSocketConnectionState.REGISTERED) { | |
307 events.onWebSocketMessage(message); | |
308 } | |
309 } | |
310 }); | |
311 } | |
312 | |
313 @Override | |
314 public void onRawTextMessage(byte[] payload) { | |
315 } | |
316 | |
317 @Override | |
318 public void onBinaryMessage(byte[] payload) { | |
319 } | |
320 } | |
321 | |
322 } | |
OLD | NEW |