SDL_Android/SmartDeviceLinkAndroidProxy - added the correct version of the proxy
[profile/ivi/smartdevicelink.git] / SDL_Android / SmartDeviceLinkProxyAndroid / src / com / smartdevicelink / transport / TCPTransport.java
1 package com.smartdevicelink.transport;\r
2 \r
3 import android.util.Log;\r
4 \r
5 import com.smartdevicelink.exception.SmartDeviceLinkException;\r
6 import com.smartdevicelink.exception.SmartDeviceLinkExceptionCause;\r
7 \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
13 \r
14 /**\r
15  * General comments:\r
16  *\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
22  *    sequences:\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
26  *\r
27  *    They can be changed to be more natural and easy to use.\r
28  *\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
33  *\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
37  */\r
38 \r
39 /**\r
40  * Class that implements TCP transport\r
41  */\r
42 public class TCPTransport extends SmartDeviceLinkTransport {\r
43 \r
44     /**\r
45      * Size of the read buffer.\r
46      */\r
47     private static final int READ_BUFFER_SIZE = 4096;\r
48 \r
49     /**\r
50      * Delay between reconnect attempts\r
51      */\r
52     private static final int RECONNECT_DELAY = 5000;\r
53 \r
54     /**\r
55      * Count of the reconnect retries\r
56      */\r
57     private static final int RECONNECT_RETRY_COUNT = 30;\r
58 \r
59     /**\r
60      * Instance of TCP transport configuration\r
61      */\r
62     private TCPTransportConfig mConfig = null;\r
63 \r
64     /**\r
65      * Instance of the client socket\r
66      */\r
67     private Socket mSocket = null;\r
68 \r
69     /**\r
70      * Instance of the input stream. Used to read data from ApplinkCore\r
71      */\r
72     private InputStream mInputStream = null;\r
73 \r
74     /**\r
75      * Instance of the output stream. Used to send data to ApplinkCore\r
76      */\r
77     private OutputStream mOutputStream = null;\r
78 \r
79     /**\r
80      * Instance of the separate thread, that does actual work, related to connecting/reading/writing data\r
81      */\r
82     private TCPTransportThread mThread = null;\r
83 \r
84     /**\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
87      */\r
88     private TCPTransportState mCurrentState = TCPTransportState.IDLE;\r
89 \r
90     /**\r
91      * Constructs TCP transport component instance\r
92      *\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
95      */\r
96     public TCPTransport(TCPTransportConfig tcpTransportConfig, ITransportListener transportListener) {\r
97         super(transportListener);\r
98         this.mConfig = tcpTransportConfig;\r
99     }\r
100 \r
101     /**\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
107      */\r
108     @Override\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
113 \r
114         boolean bResult = false;\r
115 \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
120                     try {\r
121                         mOutputStream.write(msgBytes, offset, length);\r
122                         bResult = true;\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
126                         bResult = false;\r
127                     }\r
128                 } else {\r
129                     logError("TCPTransport: sendBytesOverTransport request accepted, but output stream is null");\r
130                 }\r
131             }\r
132         } else {\r
133             logInfo("TCPTransport: sendBytesOverTransport request rejected. Transport is not connected");\r
134             bResult = false;\r
135         }\r
136 \r
137         return bResult;\r
138     }\r
139 \r
140     /**\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
144      */\r
145     @Override\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
149 \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
154                 try {\r
155                     mThread = new TCPTransportThread();\r
156                     mThread.setDaemon(true);\r
157                     mThread.start();\r
158 \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
164                 }\r
165             }\r
166         } else {\r
167             logInfo("TCPTransport: openConnection request rejected. Another connection is not finished");\r
168         }\r
169     }\r
170 \r
171 \r
172     /**\r
173      * Tries to disconnect from ApplinkCore.\r
174      * Actual try will be performed only if connection is available\r
175      */\r
176     @Override\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
180 \r
181         if(currentState == TCPTransportState.CONNECTED) {\r
182             logInfo("TCPTransport: disconnect request accepted.");\r
183             synchronized (this) {\r
184                 disconnect(null, null, true);\r
185             }\r
186         } else {\r
187             logInfo("TCPTransport: disconnect request rejected. Transport is not connected");\r
188         }\r
189     }\r
190 \r
191     /**\r
192      * Performs actual work related to disconnecting from ApplinkCore.\r
193      *\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
198      */\r
199     private synchronized void disconnect(String message, Exception exception, boolean stopThread) {\r
200 \r
201         if(getCurrentState() == TCPTransportState.DISCONNECTING) {\r
202             logInfo("TCPTransport: disconnecting already in progress");\r
203             return;\r
204         }\r
205 \r
206         setCurrentState(TCPTransportState.DISCONNECTING);\r
207 \r
208         String disconnectMsg = (message == null ? "" : message);\r
209         if (exception != null) {\r
210             disconnectMsg += ", " + exception.toString();\r
211         }\r
212 \r
213         try {\r
214             if(mThread != null && stopThread) {\r
215                 mThread.halt();\r
216                 mThread.interrupt();\r
217             }\r
218 \r
219             if(mSocket != null){\r
220                 mSocket.close();\r
221             }\r
222             mSocket = null;\r
223         } catch (IOException e) {\r
224             logError("TCPTransport.disconnect: Exception during disconnect: " + e.getMessage());\r
225         }\r
226 \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
232         } else {\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
237         }\r
238     }\r
239 \r
240     /**\r
241      * Overridden abstract method which returns specific type of this transport.\r
242      *\r
243      * @return Constant value - TransportType.TCP.\r
244      *\r
245      * @see TransportType\r
246      */\r
247     public TransportType getTransportType() {\r
248         return TransportType.TCP;\r
249     }\r
250 \r
251     /**\r
252      * Internal method for logging information messages\r
253      * @param message Message to log\r
254      */\r
255     protected void logInfo(String message) {\r
256         Log.i(getClass().getName(), message);\r
257     }\r
258 \r
259     /**\r
260      * Internal method for logging error messages\r
261      * @param message Message to log\r
262      */\r
263     protected void logError(String message) {\r
264         Log.e(getClass().getName(), message);\r
265     }\r
266 \r
267     /**\r
268      * Internal method for logging warning messages\r
269      * @param message Message to log\r
270      */\r
271     protected void logWarning(String message) {\r
272         Log.w(getClass().getName(), message);\r
273     }\r
274 \r
275     /**\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
279      */\r
280     protected void logError(String message, Throwable throwable) {\r
281         Log.e(getClass().getName(), message, throwable);\r
282     }\r
283 \r
284     /**\r
285      * Internal class that represents separate thread, that does actual work, related to connecting/reading/writing data\r
286      */\r
287     private class TCPTransportThread extends Thread {\r
288 \r
289         /**\r
290          * Represents current thread state - halted or not. This flag is used to change internal behavior depending\r
291          * on current state.\r
292          */\r
293         private Boolean isHalted = false;\r
294 \r
295         /**\r
296          * Method that marks thread as halted.\r
297          */\r
298         public void halt() {\r
299             isHalted = true;\r
300         }\r
301 \r
302         /**\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
307          *\r
308          * @return true if connection established and false otherwise\r
309          */\r
310         private boolean connect() {\r
311             boolean bConnected;\r
312             int remainingRetry = RECONNECT_RETRY_COUNT;\r
313 \r
314             synchronized (TCPTransport.this) {\r
315                 do {\r
316                     try {\r
317 \r
318                         if ((null != mSocket) && (!mSocket.isClosed())) {\r
319                             logInfo("TCPTransport.connect: Socket is not closed. Trying to close it");\r
320                             mSocket.close();\r
321                         }\r
322 \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
328 \r
329                     } catch (IOException e) {\r
330                         logError("TCPTransport.connect: Exception during connect stage: " + e.getMessage());\r
331                     }\r
332 \r
333                     bConnected = (null != mSocket) && mSocket.isConnected();\r
334 \r
335                     if(bConnected){\r
336                         logInfo("TCPTransport.connect: Socket connected");\r
337                     }else{\r
338                         if(mConfig.getAutoReconnect()){\r
339                             remainingRetry--;\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
343                         } else {\r
344                             logInfo("TCPTransport.connect: Socket not connected. AutoReconnect is OFF");\r
345                         }\r
346                     }\r
347                 } while ((!bConnected) && (mConfig.getAutoReconnect()) && (remainingRetry > 0) && (!isHalted));\r
348 \r
349                 return bConnected;\r
350             }\r
351         }\r
352 \r
353         /**\r
354          * Performs actual thread work\r
355          */\r
356         @Override\r
357         public void run() {\r
358             logInfo("TCPTransport.run: transport thread created. Starting connect stage");\r
359 \r
360             while(!isHalted) {\r
361                 setCurrentState(TCPTransportState.CONNECTING);\r
362                 if(!connect()){\r
363                     if (isHalted) {\r
364                         logInfo("TCPTransport.run: Connection failed, but thread already halted");\r
365                     } else {\r
366                         disconnect("Failed to connect to SMARTDEVICELINK", new SmartDeviceLinkException("Failed to connect to SMARTDEVICELINK"\r
367                                 , SmartDeviceLinkExceptionCause.SMARTDEVICELINK_CONNECTION_FAILED), true);\r
368                     }\r
369                     break;\r
370                 }\r
371 \r
372                 synchronized (TCPTransport.this) {\r
373                     setCurrentState(TCPTransportState.CONNECTED);\r
374                     handleTransportConnected();\r
375                 }\r
376 \r
377                 byte[] buffer = new byte[READ_BUFFER_SIZE];\r
378 \r
379                 while (!isHalted) {\r
380                     logInfo("TCPTransport.run: Waiting for data...");\r
381                     int bytesRead;\r
382                     try {\r
383                         bytesRead = mInputStream.read(buffer);\r
384                     } catch (IOException e) {\r
385                         internalHandleStreamReadError();\r
386                         break;\r
387                     }\r
388 \r
389                     synchronized (TCPTransport.this) {\r
390                         if (mThread.isInterrupted()) {\r
391                             logInfo("TCPTransport.run: Got new data but thread is interrupted");\r
392                             break;\r
393                         }\r
394                     }\r
395 \r
396                     logInfo("TCPTransport.run: Got new data");\r
397                     if (-1 == bytesRead) {\r
398                         internalHandleTCPDisconnect();\r
399                         break;\r
400                     } else if (0 == bytesRead) {\r
401                         logInfo("TCPTransport.run: Received zero bytes");\r
402                     } else {\r
403                         logInfo(String.format("TCPTransport.run: Received %d bytes", bytesRead));\r
404                         synchronized (TCPTransport.this) {\r
405                             handleReceivedBytes(buffer, bytesRead);\r
406                         }\r
407                     }\r
408                 }\r
409             }\r
410 \r
411             logInfo("TCPTransport.run: Thread terminated");\r
412             setCurrentState(TCPTransportState.IDLE);\r
413         }\r
414 \r
415         /**\r
416          * Internal handling of Tcp disconnection\r
417          */\r
418         private void internalHandleTCPDisconnect() {\r
419             if(isHalted){\r
420                 logInfo("TCPTransport.run: TCP disconnect received, but thread already halted");\r
421             } else {\r
422                 logInfo("TCPTransport.run: TCP disconnect received");\r
423                 disconnect("TCPTransport.run: End of stream reached", null, false);\r
424             }\r
425         }\r
426 \r
427         /**\r
428          * Internal handling of reading data from input stream\r
429          */\r
430         private void internalHandleStreamReadError() {\r
431             if(isHalted){\r
432                 logError("TCPTransport.run: Exception during reading data, but thread already halted");\r
433             } else {\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
437             }\r
438         }\r
439     }\r
440 \r
441     /**\r
442      * Returns current TCP transport state\r
443      *\r
444      * @return current state\r
445      */\r
446     private synchronized TCPTransportState getCurrentState() {\r
447         return mCurrentState;\r
448     }\r
449 \r
450     /**\r
451      * Sets current TCP transport state\r
452      * @param currentState New state\r
453      */\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
457     }\r
458 \r
459     /**\r
460      * Implementation of waiting required delay that cannot be interrupted\r
461      * @param timeMs Time in milliseconds of required delay\r
462      */\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
467                 try {\r
468                     wait(endTime - System.currentTimeMillis());\r
469                 } catch (Exception e) {\r
470                     // Nothing To Do, simple wait\r
471                 }\r
472             }\r
473         }\r
474     }\r
475 \r
476     /**\r
477      * Defines available states of the TCP transport\r
478      */\r
479     private enum TCPTransportState {\r
480         /**\r
481          * TCP transport is created. No connection opened\r
482          */\r
483         IDLE,\r
484 \r
485         /**\r
486          * TCP transport is in progress of establishing connection.\r
487          */\r
488         CONNECTING,\r
489 \r
490         /**\r
491          * TCP transport is connected to applink core\r
492          */\r
493         CONNECTED,\r
494 \r
495         /**\r
496          * TCP transport is in progress of disconnecting\r
497          */\r
498         DISCONNECTING\r
499     }\r
500 } // end-class\r