/* * 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.http; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import android.util.Log; import com.vodafone360.people.Settings; import com.vodafone360.people.SettingsManager; import com.vodafone360.people.engine.login.LoginEngine; import com.vodafone360.people.service.io.QueueManager; import com.vodafone360.people.service.io.rpg.RpgHeader; import com.vodafone360.people.service.io.rpg.RpgHelper; import com.vodafone360.people.service.io.rpg.RpgMessage; import com.vodafone360.people.service.io.rpg.RpgMessageTypes; import com.vodafone360.people.service.transport.DecoderThread; import com.vodafone360.people.service.transport.DecoderThread.RawResponse; import com.vodafone360.people.service.utils.AuthUtils; import com.vodafone360.people.service.utils.hessian.HessianEncoder; import com.vodafone360.people.utils.LogUtils; public class PollThread implements Runnable { private static final String RPG_MODE_KEY = "mode", RPG_MODE_ACTIVE = "active", RPG_BATCH_SIZE_KEY = "batchsize", RPG_POLLING_INTERVAL_KEY = "pollinterval"; protected static final byte ACTIVE_MODE = 1; private static final byte IDLE_MODE = 2; private static final int LONG_POLLING_INTERVAL = 30; protected static final int SHORT_POLLING_INTERVAL = 1; protected static final int DEFAULT_BATCHSIZE = 24000; private static final int NETWORK_NO_COVERAGE_RETRIES = 6; private static final long NETWORK_RETRY_INTERVAL = 30000; private HttpConnectionThread mRpgRequesterThread; private DecoderThread mDecoder; private boolean mIsConnectionRunning; private boolean mHasErrorOccured; private int mBlankHeaderCount; private int mRetryCount; private byte mMode; private int mBatchsize; private URI mUrl; private HttpClient mHttpClient; private RpgHeader mHeader; private final Object mPollLock = new Object(); private final Object mRunLock = new Object(); protected PollThread(HttpConnectionThread connThread) { mBatchsize = DEFAULT_BATCHSIZE; mRpgRequesterThread = connThread; mHasErrorOccured = false; } /** * Starts the connection. */ protected synchronized void startConnection(DecoderThread decoder) { mDecoder = decoder; setHttpClient(); mIsConnectionRunning = true; try { URL url = new URL(SettingsManager.getProperty(Settings.RPG_SERVER_KEY) + LoginEngine.getSession().userID); mUrl = url.toURI(); mHeader = new RpgHeader(); mMode = ACTIVE_MODE; Thread t = new Thread(this); t.start(); } catch (Exception e) { e.printStackTrace(); } } private void setHttpClient() { int connectionTimeout = 2 * Settings.HTTP_CONNECTION_TIMEOUT; HttpParams myHttpParams = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(myHttpParams, connectionTimeout); HttpConnectionParams.setSoTimeout(myHttpParams, connectionTimeout); mHttpClient = new DefaultHttpClient(myHttpParams); } /** * Attempts to stop the connection. This method might only take effect after * a maximum of 30 seconds when the poll returns. */ protected synchronized void stopConnection() { mIsConnectionRunning = false; synchronized (mRunLock) { mMode = IDLE_MODE; mRunLock.notify(); } mDecoder = null; mHeader = null; if (mHttpClient != null) { mHttpClient.getConnectionManager().shutdown(); mHttpClient = null; } } /** * Carries out an initial poll with a short interval to have the RPG set up * the presence roosters then does poll after poll with the default polling * interval to keep the connection alive. */ public void run() { try { invokePoll(SHORT_POLLING_INTERVAL, mBatchsize, ACTIVE_MODE); } catch (Exception e1) { e1.printStackTrace(); } while (mIsConnectionRunning) { if (mMode == ACTIVE_MODE) { synchronized (mPollLock) { mRetryCount = 0; if (mMode == ACTIVE_MODE) { try { invokePoll(LONG_POLLING_INTERVAL, mBatchsize, mMode); } catch (ClientProtocolException cpe) { mHasErrorOccured = true; } catch (IOException ioe) { mHasErrorOccured = true; } catch (Exception e) { QueueManager.getInstance().clearActiveRequests(true); LogUtils.logE("POLLTIMETEST", e); } finally { if (mIsConnectionRunning) try { Thread.sleep(100); } catch (InterruptedException ie) { } } if (mHasErrorOccured) { retryConnection(); } } } } else { // ensure the wake-up synchronized (mRunLock) { try { mRunLock.wait(); } catch (InterruptedException ie) { } } } } } /** * Invokes a poll on the RPG with the passed arguments. * * @param pollInterval The polling interval the server takes as an argument. * LONG_POLLING_INTERVAL should be used for all normal polls, * SHORT_POLLING_INTERVAL for the initial poll. * @param batchSize The maximum batch size of the client. DEFAULT_BATCHSIZE * should be used by default. * @param mode The mode to use. ACTIVE_MODE and IDLE_MODE are available. */ protected void invokePoll(int pollInterval, int batchSize, byte mode) throws Exception { if (mIsConnectionRunning) { byte[] pollData = serializeRPGPoll(pollInterval, batchSize, mode); if (pollData != null) { HttpResponse response = postHTTPRequest(pollData, mUrl, Settings.HTTP_HEADER_CONTENT_TYPE); if (mMode == ACTIVE_MODE) handleResponse(response); } } } /** * Posts an HTTP request with data to a URL under a certain content type. * The method will retry HTTP_MAX_RETRY_COUNT number of times until it * throws an exception. * * @param postData A byte array representing the data to be posted. * @param uri The URI to post the data to. * @param contentType The content type to post under. * @return The response of the server. * @throws Exception Thrown if the request failed for HTTP_MAX_RETRY_COUNT * number of times. */ private HttpResponse postHTTPRequest(byte[] postData, URI uri, String contentType) throws Exception { HttpResponse response = null; if (null == postData) { return response; } mRetryCount++; if (uri != null) { Log.d("POLLTIMETEST", "POLL Requesting URI " + uri.toString()); HttpPost httpPost = new HttpPost(uri); httpPost.addHeader("Content-Type", contentType); httpPost.setEntity(new ByteArrayEntity(postData)); Log.d("POLLTIMETEST", "POLL Requesting URI " + httpPost.getRequestLine()); try { response = mHttpClient.execute(httpPost); } catch (Exception e) { e.printStackTrace(); if (mRetryCount < Settings.HTTP_MAX_RETRY_COUNT) { postHTTPRequest(postData, uri, contentType); } else { throw e; } } } return response; } /** * <p> * Looks at the response and adds it to the necessary decoder. * </p> * TODO: this method should be worked on. The decoder should take care of * deciding which methods are decoded in which way. * * @param response The server response to decode. * @throws Exception Thrown if the returned status line was null or if the * response was null. */ private void handleResponse(HttpResponse response) throws Exception { InputStream mDataStream = null; if (response != null) { if (null != response.getStatusLine()) { int respCode = response.getStatusLine().getStatusCode(); Log.d("POLLTIMETEST", "POLL Got response status: " + respCode); switch (respCode) { case HttpStatus.SC_OK: try { mDataStream = response.getEntity().getContent(); List<RpgMessage> mRpgMessages = new ArrayList<RpgMessage>(); // Get array of RPG messages // throws IO Exception, we pass it to the calling // method RpgHelper.splitRpgResponse(mDataStream, mRpgMessages); byte[] body = null; RpgHeader rpgHeader = null; // Process each header for (RpgMessage mRpgMessage : mRpgMessages) { body = mRpgMessage.body(); rpgHeader = mRpgMessage.header(); // Determine RPG mssageType (internal response, // push // etc) int mMessageType = rpgHeader.reqType(); if (mMessageType == RpgMessageTypes.RPG_POLL_MESSAGE) { mBlankHeaderCount++; Log.e("POLLTIMETEST", "POLL handleResponse(): blank poll responses"); if (mBlankHeaderCount == Settings.BLANK_RPG_HEADER_COUNT) { Log.e("POLLTIMETEST", "POLL handleResponse(): " + Settings.BLANK_RPG_HEADER_COUNT + " blank poll responses"); stopRpgPolling(); } } else { Log.d("POLLTIMETEST", "POLL handleResponse() Non-RPG_POLL_MESSAGE"); // Reset blank header counter mBlankHeaderCount = 0; boolean mZipped = mRpgMessage.header().compression(); if (body != null && (body.length > 0)) { switch (mMessageType) { case RpgMessageTypes.RPG_EXT_RESP: // External message response Log.d("POLLTIMETEST", "POLLhandleResponse() RpgMessageTypes.RPG_EXT_RESP - " + "Add External Message RawResponse to Decode queue:" + rpgHeader.reqId() + "mBody.len=" + body.length); mDecoder.addToDecode(new RawResponse( rpgHeader.reqId(), body, mZipped, false)); break; case RpgMessageTypes.RPG_PUSH_MSG: // Define push message callback // to // notify controller Log.d("POLLTIMETEST", "POLLhandleResponse() " + "RpgMessageTypes.RPG_PUSH_MSG - " + "Add Push Message RawResponse to Decode queue:" + 0 + "mBody.len=" + body.length); mDecoder.addToDecode(new RawResponse(0, body, mZipped, true)); break; case RpgMessageTypes.RPG_INT_RESP: // Internal message response Log.d("POLLTIMETEST", "POLLhandleResponse()" + " RpgMessageTypes.RPG_INT_RESP -" + " Add RawResponse to Decode queue:" + rpgHeader.reqId() + "mBody.len=" + body.length); mDecoder.addToDecode(new RawResponse( rpgHeader.reqId(), body, mZipped, false)); break; case RpgMessageTypes.RPG_PRESENCE_RESPONSE: Log.d("POLLTIMETEST", "POLLhandleResponse() " + "RpgMessageTypes.RPG_PRESENCE_RESPONSE" + " - Add RawResponse to Decode queue - mZipped[" + mZipped + "]" + "mBody.len=" + body.length); mDecoder.addToDecode(new RawResponse( rpgHeader.reqId(), body, mZipped, false)); break; default: // Do nothing. break; } } } } } finally { if (mDataStream != null) try { mDataStream.close(); } catch (IOException e) { e.printStackTrace(); } finally { mDataStream = null; } } break; default: stopRpgPolling(); Log.e("POLLTIMETEST", "POLL handleResponse() not OK status code:" + respCode); } consumeResponse(response); } else { mMode = IDLE_MODE; throw new Exception("POLL Response status line was null."); } } else { mMode = IDLE_MODE; throw new Exception("POLL Response was null."); } } private void consumeResponse(HttpResponse response) { HttpEntity entity = response.getEntity(); if (entity != null) try { entity.consumeContent(); } catch (IOException e) { LogUtils.logE("RpgHttpPollThread.consumeResponse()", e); } } /** * Serializes a poll to hessian and includes the RPG header. * * @param pollInterval The polling interval to use. LONG_POLLING_INTERVAL or * SHORT_POLLING_INTERVAL. * @param batchSize The batch size to use on the RPG. * @param mode The mode to use. Possible values: RPG_MODE_ACTIVE, * RPG_MODE_IDLE * @return The serialized hessian-data as a byte-array. * @throws IOException */ private byte[] serializeRPGPoll(int pollInterval, int batchSize, byte mode) throws IOException { LogUtils.logD("PollThread.pollRpg()"); // hash table for parameters to Hessian encode Hashtable<String, Object> ht = new Hashtable<String, Object>(); ht.put("auth", AuthUtils.calculateAuth("", new Hashtable<String, Object>(), "" + ((long)System.currentTimeMillis() / 1000), LoginEngine.getSession())); // we only put in the polling interval if it is not the default interval if (LONG_POLLING_INTERVAL != pollInterval) { ht.put(RPG_POLLING_INTERVAL_KEY, pollInterval); } // TODO if we want to support sms wakeup in the future we should change // this to support RPG_MODE_IDLE ht.put(RPG_MODE_KEY, RPG_MODE_ACTIVE); ht.put(RPG_BATCH_SIZE_KEY, DEFAULT_BATCHSIZE); // do Hessian encoding byte[] payload = HessianEncoder.createHessianByteArray("", ht); payload[1] = (byte)1; payload[2] = (byte)0; int reqLength = RpgHeader.HEADER_LENGTH; reqLength += payload.length; mHeader.setPayloadLength(payload.length); byte[] rpgMsg = new byte[reqLength]; System.arraycopy(mHeader.createHeader(), 0, rpgMsg, 0, RpgHeader.HEADER_LENGTH); if (null != payload) { System.arraycopy(payload, 0, rpgMsg, RpgHeader.HEADER_LENGTH, payload.length); } return rpgMsg; } /** * We need to inform controller about the error here. EngineManager should * retry it's requests. */ private void stopRpgPolling() { synchronized (mRunLock) { LogUtils.logD("PollThread.stopRpgPolling()"); mMode = IDLE_MODE; // inform controller QueueManager.getInstance().clearActiveRequests(true); // reset counter mBlankHeaderCount = 0; } } /** * Starts the RPG polling in active mode. * * @throws MalformedURLException Thrown if the URL is not in the correct * format. * @throws URISyntaxException Thrown if the URL is not in the correct * format. */ protected void startRpgPolling() throws MalformedURLException, URISyntaxException { if (mMode != ACTIVE_MODE) { LogUtils.logD("PollThread.startRpgPolling()"); URL url = new URL(SettingsManager.getProperty(Settings.RPG_SERVER_KEY) + LoginEngine.getSession().userID); mUrl = url.toURI(); if (mIsConnectionRunning) { synchronized (mRunLock) { mMode = ACTIVE_MODE; mRunLock.notify(); } } } } /** * Retries to connect to the backend (if in active mode) every * NETWORK_RETRY_INTERVAL seconds. */ private void retryConnection() { int numberOfRetries = 0; while (mHasErrorOccured && (numberOfRetries <= NETWORK_NO_COVERAGE_RETRIES)) { try { invokePoll(SHORT_POLLING_INTERVAL, DEFAULT_BATCHSIZE, ACTIVE_MODE); mHasErrorOccured = false; } catch (Exception e) { try { Thread.sleep(NETWORK_RETRY_INTERVAL); } catch (Exception ee) { LogUtils.logE("PollThread.retryConnection()" + " exception thrown " + ee.toString()); } } } if (!mHasErrorOccured) { // notify requester that we have coverage again mRpgRequesterThread.notifyOfRegainedNetworkCoverage(); } else { // no coverage after several retries. let's go into idle mode mMode = IDLE_MODE; } } /** * Returns true if the device has coverage and the servers are not down. * * @return True if there is coverage. */ public boolean getHasCoverage() { return !mHasErrorOccured; } }