/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.service.transport.tcp; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.ArrayList; import java.util.List; import com.vodafone360.people.Settings; import com.vodafone360.people.SettingsManager; import com.vodafone360.people.datatypes.AuthSessionHolder; import com.vodafone360.people.engine.login.LoginEngine; import com.vodafone360.people.service.RemoteService; import com.vodafone360.people.service.io.QueueManager; import com.vodafone360.people.service.io.Request; import com.vodafone360.people.service.transport.ConnectionManager; import com.vodafone360.people.service.transport.DecoderThread; import com.vodafone360.people.service.transport.IConnection; import com.vodafone360.people.service.transport.http.HttpConnectionThread; import com.vodafone360.people.service.utils.TimeOutWatcher; import com.vodafone360.people.service.utils.hessian.HessianUtils; import com.vodafone360.people.utils.LogUtils; public class TcpConnectionThread implements Runnable, IConnection { private final Object errorLock = new Object(); private final Object requestLock = new Object(); private static final String RPG_FALLBACK_TCP_URL = "rpg.vodafone360.com"; private static final int RPG_DEFAULT_TCP_PORT = 9900; private static final int TCP_DEFAULT_TIMEOUT = 120000; private static final int ERROR_RETRY_INTERVAL = 10000; /** * If we have a connection error we try to restart after 60 seconds earliest */ private static final int CONNECTION_RESTART_INTERVAL = 60000; /** * The maximum number of retries to reestablish a connection until we sleep * until the user uses the UI or * Settings.TCP_RETRY_BROKEN_CONNECTION_INTERVAL calls another retry. */ private static final int MAX_NUMBER_RETRIES = 3; private static final int FIRST_ATTEMPT = 1; private static final int BYTE_ARRAY_OUTPUT_STREAM_SIZE = 2048; // bytes private Thread mThread; private RemoteService mService; private DecoderThread mDecoder; private boolean mConnectionShouldBeRunning; private Boolean mFailedRetrying; private Boolean mIsRetrying; private BufferedInputStream mBufferedInputStream; private OutputStream mOs; private String mRpgTcpUrl; private int mRpgTcpPort; private Socket mSocket; private HeartbeatSenderThread mHeartbeatSender; private ResponseReaderThread mResponseReader; private long mLastErrorRetryTime; private ByteArrayOutputStream mBaos; public TcpConnectionThread(DecoderThread decoder, RemoteService service) { mSocket = new Socket(); mBaos = new ByteArrayOutputStream(BYTE_ARRAY_OUTPUT_STREAM_SIZE); mIsRetrying = new Boolean(false); mFailedRetrying = new Boolean(false); mConnectionShouldBeRunning = true; mDecoder = decoder; mService = service; mLastErrorRetryTime = System.currentTimeMillis(); try { mRpgTcpUrl = SettingsManager.getProperty(Settings.TCP_RPG_URL_KEY); mRpgTcpPort = Integer.parseInt(SettingsManager.getProperty(Settings.TCP_RPG_PORT_KEY)); } catch (Exception e) { HttpConnectionThread.logE("TcpConnectionThread()", "Could not parse URL or Port!", e); mRpgTcpUrl = RPG_FALLBACK_TCP_URL; mRpgTcpPort = RPG_DEFAULT_TCP_PORT; } } public void run() { QueueManager queueManager = QueueManager.getInstance(); setFailedRetrying(false); setIsRetrying(false); try { // start the initial connection reconnectSocket(); HeartbeatSenderThread hbSender = new HeartbeatSenderThread(this, mService, mSocket); hbSender.setOutputStream(mOs); hbSender.sendHeartbeat(); hbSender = null; // TODO run this when BE supports it but keep HB in front! /* * ConnectionTester connTester = new ConnectionTester(mIs, mOs); if * (connTester.runTest()) { } else {} */ startHelperThreads(); ConnectionManager.getInstance().onConnectionStateChanged( ITcpConnectionListener.STATE_CONNECTED); } catch (IOException e) { haltAndRetryConnection(FIRST_ATTEMPT); } catch (Exception e) { haltAndRetryConnection(FIRST_ATTEMPT); } while (mConnectionShouldBeRunning) { try { if ((null != mOs) && (!getFailedRetrying())) { List<Request> reqs = QueueManager.getInstance().getRpgRequests(); int reqNum = reqs.size(); List<Integer> reqIdList = null; if (Settings.sEnableProtocolTrace || Settings.sEnableSuperExpensiveResponseFileLogging) { reqIdList = new ArrayList<Integer>(); } if (reqNum > 0) { mBaos.reset(); // batch payloads for (int i = 0; i < reqNum; i++) { Request req = reqs.get(i); if ((null == req) || (req.getAuthenticationType() == Request.USE_API)) { HttpConnectionThread.logV("TcpConnectionThread.run()", "Ignoring non-RPG method"); continue; } HttpConnectionThread.logD("TcpConnectionThread.run()", "Preparing [" + req.getRequestId() + "] for sending via RPG..."); req.setActive(true); req.writeToOutputStream(mBaos, true); // We now use the timeout mechanism for Request.Type.AVAILABILITY of request, // so we should not remove it from the queue otherwise there will be no timeout triggered. if (req.isFireAndForget() && (req.mType != Request.Type.AVAILABILITY)) { // f-a-f, no response, // remove from queue HttpConnectionThread.logD("TcpConnectionThread.run()", "Removed F&F-Request: " + req.getRequestId()); queueManager.removeRequest(req.getRequestId()); } if (Settings.sEnableProtocolTrace) { reqIdList.add(req.getRequestId()); HttpConnectionThread.logD("HttpConnectionThread.run()", "Req ID: " + req.getRequestId() + " <-> Auth: " + req.getAuth()); } } mBaos.flush(); byte[] payload = mBaos.toByteArray(); if (null != payload) { // log file containing response to SD card if (Settings.sEnableSuperExpensiveResponseFileLogging) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < reqIdList.size(); i++) { sb.append(reqIdList.get(i)); sb.append("_"); } LogUtils.logE("XXXXXXYYYXXXXXX Do not Remove this!"); LogUtils.logToFile(payload, "people_" +( reqIdList.size()>0?reqIdList.get(0):0)+ "_" + System.currentTimeMillis() + "_req_" + ((int)payload[2]) // message // type + ".txt"); } // end log file containing response to SD card if (Settings.sEnableProtocolTrace) { Long userID = null; AuthSessionHolder auth = LoginEngine.getSession(); if (auth != null) { userID = auth.userID; } HttpConnectionThread.logI("TcpConnectionThread.run()", "\n > Sending request(s) " + reqIdList.toString() + ", for user ID " + userID + " >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + HessianUtils.getInHessian( new ByteArrayInputStream(payload), true) + "\n "); } try { synchronized (mOs) { mOs.write(payload); mOs.flush(); } } catch (IOException ioe) { HttpConnectionThread.logE("TcpConnectionThread.run()", "Could not send request", ioe); notifyOfNetworkProblems(); } payload = null; } } } if (!getFailedRetrying()) { synchronized(requestLock) { requestLock.wait(); } } else { while (getFailedRetrying()) { // loop until a retry // succeeds HttpConnectionThread.logI("TcpConnectionThread.run()", "Wait() for next connection retry has started."); synchronized(errorLock) { errorLock.wait(Settings.TCP_RETRY_BROKEN_CONNECTION_INTERVAL); } if (mConnectionShouldBeRunning) { haltAndRetryConnection(FIRST_ATTEMPT); } } } } catch (Throwable t) { HttpConnectionThread.logE("TcpConnectionThread.run()", "Unknown Error: ", t); } } stopConnection(); ConnectionManager.getInstance().onConnectionStateChanged( ITcpConnectionListener.STATE_DISCONNECTED); } /** * Attempts to reconnect the socket if it has been closed for some reason. * * @throws IOException Thrown if something goes wrong while reconnecting the * socket. */ private void reconnectSocket() throws IOException { HttpConnectionThread.logI("TcpConnectionThread.reconnectSocket()", "Reconnecting Socket on " + mRpgTcpUrl + ":" + mRpgTcpPort); mSocket = null; mSocket = new Socket(); mSocket.connect(new InetSocketAddress(mRpgTcpUrl, mRpgTcpPort), TCP_DEFAULT_TIMEOUT); mBufferedInputStream = new BufferedInputStream(mSocket.getInputStream()); mOs = mSocket.getOutputStream(); HttpConnectionThread.logI("TcpConnectionThread.reconnectSocket()", "Socket started: " + mRpgTcpUrl + ":" + mRpgTcpPort); } /** * * <p> * Retries to establish a network connection after a network error has * occurred or the coverage of the network was lost. The amount of retries * depends on MAX_NUMBER_RETRIES. This method is recursive! * * </p> * <p> * A new retry is carried out each time an exception is thrown until the * limit of retries has been reached. * </p> * * @param retryIteration Shows the number of iterations we have gone through thus far. * */ private void haltAndRetryConnection(int retryIteration) { if (retryIteration < MAX_NUMBER_RETRIES) { HttpConnectionThread.logI("TcpConnectionThread.haltAndRetryConnection()", "\n \n \nRETRYING CONNECTION: " + retryIteration + " retries"); } ConnectionManager.getInstance().onConnectionStateChanged( ITcpConnectionListener.STATE_CONNECTING); if (!mConnectionShouldBeRunning) { // connection was killed by network agent HttpConnectionThread.logI("TcpConnectionThread.haltAndRetryConnection()", "Connection " + "was disconnected by Service Agent. Stopping retries!"); return; } stopConnection(); // stop to kill anything that might cause further IOEs // if we retried enough, we just return and end further retries if (retryIteration > MAX_NUMBER_RETRIES) { setFailedRetrying(true); invalidateRequests(); synchronized (requestLock) { // notify as we might be currently blocked on a request's wait() // this will cause us to go into the error lock requestLock.notify(); } synchronized (errorLock) { errorLock.notify(); } ConnectionManager.getInstance().onConnectionStateChanged( ITcpConnectionListener.STATE_DISCONNECTED); return; } try { // sleep a while to let the connection recover int sleepVal = (ERROR_RETRY_INTERVAL / 2) * retryIteration; Thread.sleep(sleepVal); } catch (InterruptedException ie) { } if (!mConnectionShouldBeRunning) { return; } try { reconnectSocket(); // TODO switch this block with the test connection block below // once the RPG implements this correctly. HeartbeatSenderThread hbSender = new HeartbeatSenderThread(this, mService, mSocket); hbSender.setOutputStream(mOs); hbSender.sendHeartbeat(); hbSender = null; setFailedRetrying(false); setIsRetrying(false); if (!mConnectionShouldBeRunning) { return; } startHelperThreads(); // restart our connections // TODO add this once the BE supports it! /* * ConnectionTester connTester = new ConnectionTester(mIs, mOs); * if (connTester.runTest()) { * mDidCriticalErrorOccur = false; * startHelperThreads(); // restart our connections Map<String, * } else { * haltAndRetryConnection(++numberOfRetries); } */ ConnectionManager.getInstance().onConnectionStateChanged( ITcpConnectionListener.STATE_CONNECTED); } catch (IOException ioe) { HttpConnectionThread.logI("TcpConnectionThread.haltAndRetryConnection()", "Failed sending heartbeat. Need to retry..."); haltAndRetryConnection(++retryIteration); } catch (Exception e) { HttpConnectionThread.logE("TcpConnectionThread.haltAndRetryConnection()", "An unknown error occured: ", e); haltAndRetryConnection(++retryIteration); } } /** * Invalidates all the requests so that the engines can either resend or * post an error message for the user. */ private void invalidateRequests() { QueueManager reqQueue = QueueManager.getInstance(); if (null != reqQueue) { TimeOutWatcher timeoutWatcher = reqQueue.getRequestTimeoutWatcher(); if (null != timeoutWatcher) { timeoutWatcher.invalidateAllRequests(); } } } @Override public void startThread() { if ((null != mThread) && (mThread.isAlive()) && (mConnectionShouldBeRunning)) { HttpConnectionThread.logI("TcpConnectionThread.startThread()", "No need to start Thread. " + "Already there. Returning"); return; } mConnectionShouldBeRunning = true; mThread = new Thread(this, "TcpConnectionThread"); mThread.start(); } @Override public void stopThread() { HttpConnectionThread.logI("TcpConnectionThread.stopThread()", "Stop Thread was called!"); mConnectionShouldBeRunning = false; synchronized (requestLock) { requestLock.notify(); } synchronized (errorLock) { errorLock.notify(); } } /** * Starts the helper threads in order to be able to read responses and send * heartbeats and passes them the needed input and output streams. */ private void startHelperThreads() { HttpConnectionThread.logI("TcpConnectionThread.startHelperThreads()", "STARTING HELPER THREADS."); if (null == mHeartbeatSender) { mHeartbeatSender = new HeartbeatSenderThread(this, mService, mSocket); HeartbeatSenderThread.mCurrentThread = mHeartbeatSender; } else { HttpConnectionThread.logE("TcpConnectionThread.startHelperThreads()", "HeartbeatSenderThread was not null!", null); } if (null == mResponseReader) { mResponseReader = new ResponseReaderThread(this, mDecoder, mSocket); ResponseReaderThread.mCurrentThread = mResponseReader; } else { HttpConnectionThread.logE("TcpConnectionThread.startHelperThreads()", "ResponseReaderThread was not null!", null); } mHeartbeatSender.setOutputStream(mOs); mResponseReader.setInputStream(mBufferedInputStream); if (!mHeartbeatSender.getIsActive()) { mHeartbeatSender.startConnection(); mResponseReader.startConnection(); } } /** * Stops the helper threads and closes the input and output streams. As the * response reader is at this point in time probably in a blocking * read()-state an IOException will need to be caught. */ private void stopHelperThreads() { HttpConnectionThread.logI("TcpConnectionThread.stopHelperThreads()", "STOPPING HELPER THREADS: " + ((null != mHeartbeatSender) ? mHeartbeatSender.getIsActive() : false)); if (null != mResponseReader) { synchronized (mResponseReader) { mResponseReader.stopConnection(); mResponseReader = null; } } if (null != mHeartbeatSender) { synchronized (mHeartbeatSender) { mHeartbeatSender.stopConnection(); mHeartbeatSender = null; } } mOs = null; mBufferedInputStream = null; } /** * Stops the connection and its underlying socket implementation. Keeps the * thread running to allow further logins from the user. */ private synchronized void stopConnection() { HttpConnectionThread.logI("TcpConnectionThread.stopConnection()", "Closing socket..."); stopHelperThreads(); if (null != mSocket) { synchronized (mSocket) { try { mSocket.close(); } catch (IOException ioe) { HttpConnectionThread.logE("TcpConnectionThread.stopConnection()", "Could not close Socket!!!!!!!!!!! This should not happen. If this fails" + "the connection might get stuck as the read() in ResponseReader might never" + "get freed!", ioe); } finally { mSocket = null; } } } QueueManager.getInstance().clearAllRequests(); } @Override public void notifyOfItemInRequestQueue() { HttpConnectionThread.logV("TcpConnectionThread.notifyOfItemInRequestQueue()", "NEW REQUEST AVAILABLE!"); synchronized (requestLock) { requestLock.notify(); } synchronized (errorLock) { errorLock.notify(); } } /** * Gets notified whenever the user is using the UI. In this special case * whenever an error occured with the connection and this method was not * called a short time before */ @Override public void notifyOfUiActivity() { if (getFailedRetrying()) { if ((System.currentTimeMillis() - mLastErrorRetryTime) >= CONNECTION_RESTART_INTERVAL) { synchronized (errorLock) { // if we are in an error state let's try to fix it errorLock.notify(); } mLastErrorRetryTime = System.currentTimeMillis(); } } } @Override public void notifyOfRegainedNetworkCoverage() { synchronized (errorLock) { errorLock.notify(); } } /** * Called back by the response reader, which should notice network problems * first */ protected void notifyOfNetworkProblems() { if(getIsRetrying()) { return; } setIsRetrying(true); HttpConnectionThread.logE("TcpConnectionThread.notifyOfNetworkProblems()", "Houston, we have a network problem!", null); haltAndRetryConnection(FIRST_ATTEMPT); } @Override public boolean getIsConnected() { return mConnectionShouldBeRunning; } @Override public boolean getIsRpgConnectionActive() { if ((null != mHeartbeatSender) && (mHeartbeatSender.getIsActive())) { return true; } return false; } /** * * If the connection was lost this flag indicates that we are trying to regain it. * * @param isRetrying True if the connection is currently trying to be regained, false otherwise. * */ private void setIsRetrying(final boolean isRetrying) { synchronized(mIsRetrying) { mIsRetrying = isRetrying; } } /** * * Sets a flag which indicates whether the connection has failed retrying to connect to the server * for 3 consecutive times or not. * * @param failedRetrying True if the connection failed to connect 3 times, otherwise false. */ private void setFailedRetrying(final boolean failedRetrying) { synchronized(mFailedRetrying) { mFailedRetrying = failedRetrying; } } /** * * Returns whether we are currently trying to regain a connection. * * @return True if we are trying to regain the connection, false otherwise. * */ private boolean getIsRetrying() { synchronized(mIsRetrying) { return mIsRetrying; } } /** * * Returns whether we have failed trying to regain the connection (happens after 3 retries). * * @return True if we have failed reconnecting, false if we are connected. */ private boolean getFailedRetrying() { synchronized(mFailedRetrying) { return mFailedRetrying; } } @Override public void onLoginStateChanged(boolean isLoggedIn) { } }