/****************************************************************************** * * Copyright 2011-2012 Tavendo GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package org.magnum.soda.transport.wamp; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.channels.SocketChannel; import android.os.AsyncTask; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.util.Log; public class WebSocketConnection implements WebSocket { private static final boolean DEBUG = true; private static final String TAG = WebSocketConnection.class.getName(); protected Handler mMasterHandler; protected WebSocketReader mReader; protected WebSocketWriter mWriter; protected HandlerThread mWriterThread; protected SocketChannel mTransportChannel; private URI mWsUri; private String mWsScheme; private String mWsHost; private int mWsPort; private String mWsPath; private String mWsQuery; private String[] mWsSubprotocols; private WebSocket.ConnectionHandler mWsHandler; protected WebSocketOptions mOptions; private boolean mActive; private boolean mPrevConnected; /** * Asynch socket connector. */ private class WebSocketConnector extends AsyncTask<Void, Void, String> { @Override protected String doInBackground(Void... params) { Thread.currentThread().setName("WebSocketConnector"); // connect TCP socket // http://developer.android.com/reference/java/nio/channels/SocketChannel.html // try { mTransportChannel = SocketChannel.open(); // the following will block until connection was established or an error occurred! mTransportChannel.socket().connect(new InetSocketAddress(mWsHost, mWsPort), mOptions.getSocketConnectTimeout()); // before doing any data transfer on the socket, set socket options mTransportChannel.socket().setSoTimeout(mOptions.getSocketReceiveTimeout()); mTransportChannel.socket().setTcpNoDelay(mOptions.getTcpNoDelay()); return null; } catch (IOException e) { return e.getMessage(); } } @Override protected void onPostExecute(String reason) { if (reason != null) { onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT, reason); } else if (mTransportChannel.isConnected()) { try { // create & start WebSocket reader createReader(); // create & start WebSocket writer createWriter(); // start WebSockets handshake WebSocketMessage.ClientHandshake hs = new WebSocketMessage.ClientHandshake(mWsHost + ":" + mWsPort); hs.mPath = mWsPath; hs.mQuery = mWsQuery; hs.mSubprotocols = mWsSubprotocols; mWriter.forward(hs); mPrevConnected = true; } catch (Exception e) { onClose(WebSocketConnectionHandler.CLOSE_INTERNAL_ERROR, e.getMessage()); } } else { onClose(WebSocketConnectionHandler.CLOSE_CANNOT_CONNECT, "could not connect to WebSockets server"); } } } public WebSocketConnection() { if (DEBUG) Log.d(TAG, "created"); // create WebSocket master handler createHandler(); // set initial values mActive = false; mPrevConnected = false; } public void sendTextMessage(String payload) { mWriter.forward(new WebSocketMessage.TextMessage(payload)); } public void sendRawTextMessage(byte[] payload) { mWriter.forward(new WebSocketMessage.RawTextMessage(payload)); } public void sendBinaryMessage(byte[] payload) { mWriter.forward(new WebSocketMessage.BinaryMessage(payload)); } public boolean isConnected() { return mTransportChannel != null && mTransportChannel.isConnected(); } private void failConnection(int code, String reason) { if (DEBUG) Log.d(TAG, "fail connection [code = " + code + ", reason = " + reason); if (mReader != null) { mReader.quit(); try { mReader.join(); } catch (InterruptedException e) { if (DEBUG) e.printStackTrace(); } //mReader = null; } else { if (DEBUG) Log.d(TAG, "mReader already NULL"); } if (mWriter != null) { //mWriterThread.getLooper().quit(); mWriter.forward(new WebSocketMessage.Quit()); try { mWriterThread.join(); } catch (InterruptedException e) { if (DEBUG) e.printStackTrace(); } //mWriterThread = null; } else { if (DEBUG) Log.d(TAG, "mWriter already NULL"); } if (mTransportChannel != null) { try { mTransportChannel.close(); } catch (IOException e) { if (DEBUG) e.printStackTrace(); } //mTransportChannel = null; } else { if (DEBUG) Log.d(TAG, "mTransportChannel already NULL"); } onClose(code, reason); if (DEBUG) Log.d(TAG, "worker threads stopped"); } public void connect(String wsUri, WebSocket.ConnectionHandler wsHandler) throws WebSocketException { connect(wsUri, null, wsHandler, new WebSocketOptions()); } public void connect(String wsUri, WebSocket.ConnectionHandler wsHandler, WebSocketOptions options) throws WebSocketException { connect(wsUri, null, wsHandler, options); } public void connect(String wsUri, String[] wsSubprotocols, WebSocket.ConnectionHandler wsHandler, WebSocketOptions options) throws WebSocketException { // don't connect if already connected .. user needs to disconnect first // if (mTransportChannel != null && mTransportChannel.isConnected()) { throw new WebSocketException("already connected"); } // parse WebSockets URI // try { mWsUri = new URI(wsUri); if (!mWsUri.getScheme().equals("ws") && !mWsUri.getScheme().equals("wss")) { throw new WebSocketException("unsupported scheme for WebSockets URI"); } if (mWsUri.getScheme().equals("wss")) { throw new WebSocketException("secure WebSockets not implemented"); } mWsScheme = mWsUri.getScheme(); if (mWsUri.getPort() == -1) { if (mWsScheme.equals("ws")) { mWsPort = 80; } else { mWsPort = 443; } } else { mWsPort = mWsUri.getPort(); } if (mWsUri.getHost() == null) { throw new WebSocketException("no host specified in WebSockets URI"); } else { mWsHost = mWsUri.getHost(); } if (mWsUri.getPath() == null || mWsUri.getPath().equals("")) { mWsPath = "/"; } else { mWsPath = mWsUri.getPath(); } if (mWsUri.getQuery() == null || mWsUri.getQuery().equals("")) { mWsQuery = null; } else { mWsQuery = mWsUri.getQuery(); } } catch (URISyntaxException e) { throw new WebSocketException("invalid WebSockets URI"); } mWsSubprotocols = wsSubprotocols; mWsHandler = wsHandler; // make copy of options! mOptions = new WebSocketOptions(options); // set connection active mActive = true; // use asynch connector on short-lived background thread new WebSocketConnector().execute(); } public void disconnect() { if (mWriter != null) { mWriter.forward(new WebSocketMessage.Close(1000)); } else { if (DEBUG) Log.d(TAG, "could not send Close .. writer already NULL"); } mActive = false; mPrevConnected = false; } /** * Reconnect to the server with the latest options * @return true if reconnection performed */ public boolean reconnect() { if (!isConnected() && (mWsUri != null)) { new WebSocketConnector().execute(); return true; } return false; } /** * Perform reconnection * * @return true if reconnection was scheduled */ protected boolean scheduleReconnect() { /** * Reconnect only if: * - connection active (connected but not disconnected) * - has previous success connections * - reconnect interval is set */ int interval = mOptions.getReconnectInterval(); boolean need = mActive && mPrevConnected && (interval > 0); if (need) { if (DEBUG) Log.d(TAG, "Reconnection scheduled"); mMasterHandler.postDelayed(new Runnable() { public void run() { if (DEBUG) Log.d(TAG, "Reconnecting..."); reconnect(); } }, interval); } return need; } /** * Common close handler * * @param code Close code. * @param reason Close reason (human-readable). */ private void onClose(int code, String reason) { boolean reconnecting = false; if ((code == WebSocket.ConnectionHandler.CLOSE_CANNOT_CONNECT) || (code == WebSocket.ConnectionHandler.CLOSE_CONNECTION_LOST)) { reconnecting = scheduleReconnect(); } if (mWsHandler != null) { try { if (reconnecting) { mWsHandler.onClose(WebSocket.ConnectionHandler.CLOSE_RECONNECT, reason); } else { mWsHandler.onClose(code, reason); } } catch (Exception e) { if (DEBUG) e.printStackTrace(); } //mWsHandler = null; } else { if (DEBUG) Log.d(TAG, "mWsHandler already NULL"); } } /** * Create master message handler. */ protected void createHandler() { mMasterHandler = new Handler() { public void handleMessage(Message msg) { if (msg.obj instanceof WebSocketMessage.TextMessage) { WebSocketMessage.TextMessage textMessage = (WebSocketMessage.TextMessage) msg.obj; if (mWsHandler != null) { mWsHandler.onTextMessage(textMessage.mPayload); } else { if (DEBUG) Log.d(TAG, "could not call onTextMessage() .. handler already NULL"); } } else if (msg.obj instanceof WebSocketMessage.RawTextMessage) { WebSocketMessage.RawTextMessage rawTextMessage = (WebSocketMessage.RawTextMessage) msg.obj; if (mWsHandler != null) { mWsHandler.onRawTextMessage(rawTextMessage.mPayload); } else { if (DEBUG) Log.d(TAG, "could not call onRawTextMessage() .. handler already NULL"); } } else if (msg.obj instanceof WebSocketMessage.BinaryMessage) { WebSocketMessage.BinaryMessage binaryMessage = (WebSocketMessage.BinaryMessage) msg.obj; if (mWsHandler != null) { mWsHandler.onBinaryMessage(binaryMessage.mPayload); } else { if (DEBUG) Log.d(TAG, "could not call onBinaryMessage() .. handler already NULL"); } } else if (msg.obj instanceof WebSocketMessage.Ping) { WebSocketMessage.Ping ping = (WebSocketMessage.Ping) msg.obj; if (DEBUG) Log.d(TAG, "WebSockets Ping received"); // reply with Pong WebSocketMessage.Pong pong = new WebSocketMessage.Pong(); pong.mPayload = ping.mPayload; mWriter.forward(pong); } else if (msg.obj instanceof WebSocketMessage.Pong) { @SuppressWarnings("unused") WebSocketMessage.Pong pong = (WebSocketMessage.Pong) msg.obj; if (DEBUG) Log.d(TAG, "WebSockets Pong received"); } else if (msg.obj instanceof WebSocketMessage.Close) { WebSocketMessage.Close close = (WebSocketMessage.Close) msg.obj; if (DEBUG) Log.d(TAG, "WebSockets Close received (" + close.mCode + " - " + close.mReason + ")"); mWriter.forward(new WebSocketMessage.Close(1000)); } else if (msg.obj instanceof WebSocketMessage.ServerHandshake) { WebSocketMessage.ServerHandshake serverHandshake = (WebSocketMessage.ServerHandshake) msg.obj; if (DEBUG) Log.d(TAG, "opening handshake received"); if (serverHandshake.mSuccess) { if (mWsHandler != null) { mWsHandler.onOpen(); } else { if (DEBUG) Log.d(TAG, "could not call onOpen() .. handler already NULL"); } } } else if (msg.obj instanceof WebSocketMessage.ConnectionLost) { @SuppressWarnings("unused") WebSocketMessage.ConnectionLost connnectionLost = (WebSocketMessage.ConnectionLost) msg.obj; failConnection(WebSocketConnectionHandler.CLOSE_CONNECTION_LOST, "WebSockets connection lost"); } else if (msg.obj instanceof WebSocketMessage.ProtocolViolation) { @SuppressWarnings("unused") WebSocketMessage.ProtocolViolation protocolViolation = (WebSocketMessage.ProtocolViolation) msg.obj; failConnection(WebSocketConnectionHandler.CLOSE_PROTOCOL_ERROR, "WebSockets protocol violation"); } else if (msg.obj instanceof WebSocketMessage.Error) { WebSocketMessage.Error error = (WebSocketMessage.Error) msg.obj; failConnection(WebSocketConnectionHandler.CLOSE_INTERNAL_ERROR, "WebSockets internal error (" + error.mException.toString() + ")"); } else if (msg.obj instanceof WebSocketMessage.ServerError) { WebSocketMessage.ServerError error = (WebSocketMessage.ServerError) msg.obj; failConnection(WebSocketConnectionHandler.CLOSE_SERVER_ERROR, "Server error " + error.mStatusCode + " (" + error.mStatusMessage + ")"); } else { processAppMessage(msg.obj); } } }; } protected void processAppMessage(Object message) { } /** * Create WebSockets background writer. */ protected void createWriter() { mWriterThread = new HandlerThread("WebSocketWriter"); mWriterThread.start(); mWriter = new WebSocketWriter(mWriterThread.getLooper(), mMasterHandler, mTransportChannel, mOptions); if (DEBUG) Log.d(TAG, "WS writer created and started"); } /** * Create WebSockets background reader. */ protected void createReader() { mReader = new WebSocketReader(mMasterHandler, mTransportChannel, mOptions, "WebSocketReader"); mReader.start(); if (DEBUG) Log.d(TAG, "WS reader created and started"); } }