| 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 |