1 package com.smartdevicelink.transport;
\r
3 import android.util.Log;
\r
5 import com.smartdevicelink.exception.SmartDeviceLinkException;
\r
6 import com.smartdevicelink.exception.SmartDeviceLinkExceptionCause;
\r
8 import java.io.IOException;
\r
9 import java.io.InputStream;
\r
10 import java.io.OutputStream;
\r
11 import java.net.InetSocketAddress;
\r
12 import java.net.Socket;
\r
17 * 1) Transport layer can be reorganized to properly incapsulate thread-related code according to Android OS guidelines
\r
18 * 2) Currently there are different cases when transport error information sent to listener components
\r
19 * a) when there are some errors during writing data to transport
\r
20 * b) when there are errors during connection establishing/reading data
\r
21 * But information about transport disconnection is sent only if disconnection was successful. So we have following
\r
23 * a) handleTransportConnected -> read without errors -> handleTransportDisconnected
\r
24 * b) handleTransportConnected -> handleTransportError(write with errors) -> handleTransportDisconnected
\r
25 * c) handleTransportConnected -> handleTransportError(read with errors) -> handleTransportError(socket closed)
\r
27 * They can be changed to be more natural and easy to use.
\r
29 * 3) Public api is inconsistent. During single call of some api method (for example "openConnection") some of the
\r
30 * following result can appears:
\r
31 * a) SmartDeviceLinkException thrown
\r
32 * b) onTransportError callback called on listeners
\r
34 * 4) Handling of connection must be more stable
\r
35 * 5) General solution in case of reconnecting must be implemented
\r
36 * 6) It must be common and same solution for handling information about physical device adapters (BT, WiFi etc.)
\r
40 * Class that implements TCP transport
\r
42 public class TCPTransport extends SmartDeviceLinkTransport {
\r
45 * Size of the read buffer.
\r
47 private static final int READ_BUFFER_SIZE = 4096;
\r
50 * Delay between reconnect attempts
\r
52 private static final int RECONNECT_DELAY = 5000;
\r
55 * Count of the reconnect retries
\r
57 private static final int RECONNECT_RETRY_COUNT = 30;
\r
60 * Instance of TCP transport configuration
\r
62 private TCPTransportConfig mConfig = null;
\r
65 * Instance of the client socket
\r
67 private Socket mSocket = null;
\r
70 * Instance of the input stream. Used to read data from ApplinkCore
\r
72 private InputStream mInputStream = null;
\r
75 * Instance of the output stream. Used to send data to ApplinkCore
\r
77 private OutputStream mOutputStream = null;
\r
80 * Instance of the separate thread, that does actual work, related to connecting/reading/writing data
\r
82 private TCPTransportThread mThread = null;
\r
85 * Initial internal state of the component. Used like a simple lightweight FSM replacement while component
\r
86 * must behave differently in response to it's public function calls depending of it's current state
\r
88 private TCPTransportState mCurrentState = TCPTransportState.IDLE;
\r
91 * Constructs TCP transport component instance
\r
93 * @param tcpTransportConfig Instance of the TCP transport configuration
\r
94 * @param transportListener Listener that will be notified on different TCP transport activities
\r
96 public TCPTransport(TCPTransportConfig tcpTransportConfig, ITransportListener transportListener) {
\r
97 super(transportListener);
\r
98 this.mConfig = tcpTransportConfig;
\r
102 * Performs actual work of sending array of bytes over the transport
\r
103 * @param msgBytes Bytes to send
\r
104 * @param offset Offset in the bytes array to send data from
\r
105 * @param length Number of bytes to send
\r
106 * @return True if data was sent successfully, False otherwise
\r
109 protected boolean sendBytesOverTransport(byte[] msgBytes, int offset, int length) {
\r
110 TCPTransportState currentState = getCurrentState();
\r
111 logInfo(String.format("TCPTransport: sendBytesOverTransport requested. Size: %d, Offset: %d, Length: %d, Current state is: %s"
\r
112 , msgBytes.length, offset, length, currentState.name()));
\r
114 boolean bResult = false;
\r
116 if(currentState == TCPTransportState.CONNECTED) {
\r
117 synchronized (this) {
\r
118 if (mOutputStream != null) {
\r
119 logInfo("TCPTransport: sendBytesOverTransport request accepted. Trying to send data");
\r
121 mOutputStream.write(msgBytes, offset, length);
\r
123 logInfo("TCPTransport.sendBytesOverTransport: successfully send data");
\r
124 } catch (IOException e) {
\r
125 logError("TCPTransport.sendBytesOverTransport: error during sending data: " + e.getMessage());
\r
129 logError("TCPTransport: sendBytesOverTransport request accepted, but output stream is null");
\r
133 logInfo("TCPTransport: sendBytesOverTransport request rejected. Transport is not connected");
\r
141 * Tries to open connection to ApplinkCore.
\r
142 * Actual try will be performed only if no actual connection is available
\r
143 * @throws SmartDeviceLinkException
\r
146 public void openConnection() throws SmartDeviceLinkException {
\r
147 TCPTransportState currentState = getCurrentState();
\r
148 logInfo(String.format("TCPTransport: openConnection requested. Current state is: %s", currentState.name()));
\r
150 if(currentState == TCPTransportState.IDLE) {
\r
151 synchronized (this) {
\r
152 setCurrentState(TCPTransportState.CONNECTING);
\r
153 logInfo("TCPTransport: openConnection request accepted. Starting transport thread");
\r
155 mThread = new TCPTransportThread();
\r
156 mThread.setDaemon(true);
\r
159 // Initialize the SiphonServer
\r
160 SiphonServer.init();
\r
161 } catch (Exception e) {
\r
162 logError("TCPTransport: Exception during transport thread starting", e);
\r
163 throw new SmartDeviceLinkException(e);
\r
167 logInfo("TCPTransport: openConnection request rejected. Another connection is not finished");
\r
173 * Tries to disconnect from ApplinkCore.
\r
174 * Actual try will be performed only if connection is available
\r
177 public void disconnect() {
\r
178 TCPTransportState currentState = getCurrentState();
\r
179 logInfo(String.format("TCPTransport: disconnect requested from client. Current state is: %s", currentState.name()));
\r
181 if(currentState == TCPTransportState.CONNECTED) {
\r
182 logInfo("TCPTransport: disconnect request accepted.");
\r
183 synchronized (this) {
\r
184 disconnect(null, null, true);
\r
187 logInfo("TCPTransport: disconnect request rejected. Transport is not connected");
\r
192 * Performs actual work related to disconnecting from ApplinkCore.
\r
194 * @param message Message that describes disconnect reason
\r
195 * @param exception Some of the possible exceptions that was the reason of disconnecting
\r
196 * @param stopThread True if not only disconnection must be done but also thread that handles connection must be
\r
197 * also stopped so no reconnect attempts will be made
\r
199 private synchronized void disconnect(String message, Exception exception, boolean stopThread) {
\r
201 if(getCurrentState() == TCPTransportState.DISCONNECTING) {
\r
202 logInfo("TCPTransport: disconnecting already in progress");
\r
206 setCurrentState(TCPTransportState.DISCONNECTING);
\r
208 String disconnectMsg = (message == null ? "" : message);
\r
209 if (exception != null) {
\r
210 disconnectMsg += ", " + exception.toString();
\r
214 if(mThread != null && stopThread) {
\r
216 mThread.interrupt();
\r
219 if(mSocket != null){
\r
223 } catch (IOException e) {
\r
224 logError("TCPTransport.disconnect: Exception during disconnect: " + e.getMessage());
\r
227 if (exception == null) {
\r
228 // This disconnect was not caused by an error, notify the proxy that
\r
229 // the transport has been disconnected.
\r
230 logInfo("Disconnect is correct. Handling it");
\r
231 handleTransportDisconnected(disconnectMsg);
\r
233 // This disconnect was caused by an error, notify the proxy
\r
234 // that there was a transport error.
\r
235 logError("Disconnect is incorrect. Handling it as error");
\r
236 handleTransportError(disconnectMsg, exception);
\r
241 * Overridden abstract method which returns specific type of this transport.
\r
243 * @return Constant value - TransportType.TCP.
\r
245 * @see TransportType
\r
247 public TransportType getTransportType() {
\r
248 return TransportType.TCP;
\r
252 * Internal method for logging information messages
\r
253 * @param message Message to log
\r
255 protected void logInfo(String message) {
\r
256 Log.i(getClass().getName(), message);
\r
260 * Internal method for logging error messages
\r
261 * @param message Message to log
\r
263 protected void logError(String message) {
\r
264 Log.e(getClass().getName(), message);
\r
268 * Internal method for logging warning messages
\r
269 * @param message Message to log
\r
271 protected void logWarning(String message) {
\r
272 Log.w(getClass().getName(), message);
\r
276 * Internal method for logging error message together with information about exception that was the reason of it
\r
277 * @param message Message to log
\r
278 * @param throwable Exception, that was the main reason for logged error message
\r
280 protected void logError(String message, Throwable throwable) {
\r
281 Log.e(getClass().getName(), message, throwable);
\r
285 * Internal class that represents separate thread, that does actual work, related to connecting/reading/writing data
\r
287 private class TCPTransportThread extends Thread {
\r
290 * Represents current thread state - halted or not. This flag is used to change internal behavior depending
\r
291 * on current state.
\r
293 private Boolean isHalted = false;
\r
296 * Method that marks thread as halted.
\r
298 public void halt() {
\r
303 * Tries to connect to the applink core. Behavior depends autoReconnect configuration param:
\r
304 * a) If autoReconnect is false, then only one connect try will be performed.
\r
305 * b) If autoReconnect is true, then in case of connection error continuous reconnect will be performed
\r
306 * after short delay until connection will be established or retry count will be reached
\r
308 * @return true if connection established and false otherwise
\r
310 private boolean connect() {
\r
311 boolean bConnected;
\r
312 int remainingRetry = RECONNECT_RETRY_COUNT;
\r
314 synchronized (TCPTransport.this) {
\r
318 if ((null != mSocket) && (!mSocket.isClosed())) {
\r
319 logInfo("TCPTransport.connect: Socket is not closed. Trying to close it");
\r
323 logInfo(String.format("TCPTransport.connect: Socket is closed. Trying to connect to %s", mConfig));
\r
324 mSocket = new Socket();
\r
325 mSocket.connect(new InetSocketAddress(mConfig.getIPAddress(), mConfig.getPort()));
\r
326 mOutputStream = mSocket.getOutputStream();
\r
327 mInputStream = mSocket.getInputStream();
\r
329 } catch (IOException e) {
\r
330 logError("TCPTransport.connect: Exception during connect stage: " + e.getMessage());
\r
333 bConnected = (null != mSocket) && mSocket.isConnected();
\r
336 logInfo("TCPTransport.connect: Socket connected");
\r
338 if(mConfig.getAutoReconnect()){
\r
340 logInfo(String.format("TCPTransport.connect: Socket not connected. AutoReconnect is ON. retryCount is: %d. Waiting for reconnect delay: %d"
\r
341 , remainingRetry, RECONNECT_DELAY));
\r
342 waitFor(RECONNECT_DELAY);
\r
344 logInfo("TCPTransport.connect: Socket not connected. AutoReconnect is OFF");
\r
347 } while ((!bConnected) && (mConfig.getAutoReconnect()) && (remainingRetry > 0) && (!isHalted));
\r
354 * Performs actual thread work
\r
357 public void run() {
\r
358 logInfo("TCPTransport.run: transport thread created. Starting connect stage");
\r
361 setCurrentState(TCPTransportState.CONNECTING);
\r
364 logInfo("TCPTransport.run: Connection failed, but thread already halted");
\r
366 disconnect("Failed to connect to SMARTDEVICELINK", new SmartDeviceLinkException("Failed to connect to SMARTDEVICELINK"
\r
367 , SmartDeviceLinkExceptionCause.SMARTDEVICELINK_CONNECTION_FAILED), true);
\r
372 synchronized (TCPTransport.this) {
\r
373 setCurrentState(TCPTransportState.CONNECTED);
\r
374 handleTransportConnected();
\r
377 byte[] buffer = new byte[READ_BUFFER_SIZE];
\r
379 while (!isHalted) {
\r
380 logInfo("TCPTransport.run: Waiting for data...");
\r
383 bytesRead = mInputStream.read(buffer);
\r
384 } catch (IOException e) {
\r
385 internalHandleStreamReadError();
\r
389 synchronized (TCPTransport.this) {
\r
390 if (mThread.isInterrupted()) {
\r
391 logInfo("TCPTransport.run: Got new data but thread is interrupted");
\r
396 logInfo("TCPTransport.run: Got new data");
\r
397 if (-1 == bytesRead) {
\r
398 internalHandleTCPDisconnect();
\r
400 } else if (0 == bytesRead) {
\r
401 logInfo("TCPTransport.run: Received zero bytes");
\r
403 logInfo(String.format("TCPTransport.run: Received %d bytes", bytesRead));
\r
404 synchronized (TCPTransport.this) {
\r
405 handleReceivedBytes(buffer, bytesRead);
\r
411 logInfo("TCPTransport.run: Thread terminated");
\r
412 setCurrentState(TCPTransportState.IDLE);
\r
416 * Internal handling of Tcp disconnection
\r
418 private void internalHandleTCPDisconnect() {
\r
420 logInfo("TCPTransport.run: TCP disconnect received, but thread already halted");
\r
422 logInfo("TCPTransport.run: TCP disconnect received");
\r
423 disconnect("TCPTransport.run: End of stream reached", null, false);
\r
428 * Internal handling of reading data from input stream
\r
430 private void internalHandleStreamReadError() {
\r
432 logError("TCPTransport.run: Exception during reading data, but thread already halted");
\r
434 logError("TCPTransport.run: Exception during reading data");
\r
435 disconnect("Failed to read data from SMARTDEVICELINK", new SmartDeviceLinkException("Failed to read data from SMARTDEVICELINK"
\r
436 , SmartDeviceLinkExceptionCause.SMARTDEVICELINK_CONNECTION_FAILED), false);
\r
442 * Returns current TCP transport state
\r
444 * @return current state
\r
446 private synchronized TCPTransportState getCurrentState() {
\r
447 return mCurrentState;
\r
451 * Sets current TCP transport state
\r
452 * @param currentState New state
\r
454 private synchronized void setCurrentState(TCPTransportState currentState) {
\r
455 logInfo(String.format("Current state changed to: %s", currentState));
\r
456 this.mCurrentState = currentState;
\r
460 * Implementation of waiting required delay that cannot be interrupted
\r
461 * @param timeMs Time in milliseconds of required delay
\r
463 private void waitFor(long timeMs) {
\r
464 long endTime = System.currentTimeMillis() +timeMs;
\r
465 while (System.currentTimeMillis() < endTime) {
\r
466 synchronized (this) {
\r
468 wait(endTime - System.currentTimeMillis());
\r
469 } catch (Exception e) {
\r
470 // Nothing To Do, simple wait
\r
477 * Defines available states of the TCP transport
\r
479 private enum TCPTransportState {
\r
481 * TCP transport is created. No connection opened
\r
486 * TCP transport is in progress of establishing connection.
\r
491 * TCP transport is connected to applink core
\r
496 * TCP transport is in progress of disconnecting
\r