/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 The Android Open Source Project
*
* 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 com.android.im.imps;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import android.net.http.AndroidHttpClient;
import android.os.SystemClock;
import android.util.Log;
import com.android.im.engine.HeartbeatService;
import com.android.im.engine.ImErrorInfo;
import com.android.im.engine.ImException;
import com.android.im.engine.SystemService;
import com.android.im.imps.Primitive.TransactionMode;
/**
* The <code>HttpDataChannel</code> is an implementation of IMPS data channel
* in which the protocol binding is HTTP.
*/
class HttpDataChannel extends DataChannel implements Runnable, HeartbeatService.Callback {
private static final int MAX_RETRY_COUNT = 10;
private static final int INIT_RETRY_DELAY_MS = 5000;
private static final int MAX_RETRY_DELAY_MS = 300 * 1000;
private Thread mSendThread;
private boolean mStopped;
private boolean mSuspended;
private boolean mConnected;
private boolean mStopRetry;
private Object mRetryLock = new Object();
private LinkedBlockingQueue<Primitive> mSendQueue;
private LinkedBlockingQueue<Primitive> mReceiveQueue;
private long mLastActive;
private long mKeepAliveMillis;
private Primitive mKeepAlivePrimitive;
private AtomicBoolean mHasPendingPolling = new AtomicBoolean(false);
private final AndroidHttpClient mHttpClient;
private final Header mContentTypeHeader;
private final Header mMsisdnHeader;
private URI mPostUri;
private ImpsTransactionManager mTxManager;
/**
* Constructs a new HttpDataChannel for a connection.
*
* @param connection the connection which uses the data channel.
*/
public HttpDataChannel(ImpsConnection connection) throws ImException {
super(connection);
mTxManager = connection.getTransactionManager();
ImpsConnectionConfig cfg = connection.getConfig();
try {
String host = cfg.getHost();
if (host == null || host.length() == 0) {
throw new ImException(ImErrorInfo.INVALID_HOST_NAME,
"Empty host name.");
}
mPostUri = new URI(cfg.getHost());
if (mPostUri.getPath() == null || "".equals(mPostUri.getPath())) {
mPostUri = new URI(cfg.getHost() + "/");
}
if (!"http".equalsIgnoreCase(mPostUri.getScheme())
&& !"https".equalsIgnoreCase(mPostUri.getScheme())) {
throw new ImException(ImErrorInfo.INVALID_HOST_NAME,
"Non HTTP/HTTPS host name.");
}
mHttpClient = AndroidHttpClient.newInstance("Android-Imps/0.1");
HttpParams params = mHttpClient.getParams();
HttpConnectionParams.setConnectionTimeout(params, cfg.getReplyTimeout());
HttpConnectionParams.setSoTimeout(params, cfg.getReplyTimeout());
} catch (URISyntaxException e) {
throw new ImException(ImErrorInfo.INVALID_HOST_NAME,
e.getLocalizedMessage());
}
mContentTypeHeader = new BasicHeader("Content-Type", cfg.getTransportContentType());
String msisdn = cfg.getMsisdn();
mMsisdnHeader = (msisdn != null) ? new BasicHeader("MSISDN", msisdn) : null;
mParser = cfg.createPrimitiveParser();
mSerializer = cfg.createPrimitiveSerializer();
}
@Override
public void connect() throws ImException {
if (mConnected) {
throw new ImException("Already connected");
}
mStopped = false;
mStopRetry = false;
mSendQueue = new LinkedBlockingQueue<Primitive>();
mReceiveQueue = new LinkedBlockingQueue<Primitive>();
mSendThread = new Thread(this, "HttpDataChannel");
mSendThread.setDaemon(true);
mSendThread.start();
mConnected = true;
}
@Override
public void suspend() {
mSuspended = true;
}
@Override
public boolean resume() {
long now = SystemClock.elapsedRealtime();
if (now - mLastActive > mKeepAliveMillis) {
shutdown();
return false;
} else {
mSuspended = false;
// Send a polling request after resume in case we missed some
// updates while we are suspended.
Primitive polling = new Primitive(ImpsTags.Polling_Request);
polling.setSession(mConnection.getSession().getID());
sendPrimitive(polling);
startHeartbeat();
return true;
}
}
@Override
public void shutdown() {
HeartbeatService heartbeatService
= SystemService.getDefault().getHeartbeatService();
if (heartbeatService != null) {
heartbeatService.stopHeartbeat(this);
}
// Stop the sending thread
mStopped = true;
mSendThread.interrupt();
mConnected = false;
}
@Override
public void sendPrimitive(Primitive p) {
if (!mConnected || mStopped) {
ImpsLog.log("DataChannel not connected, ignore primitive " + p.getType());
return;
}
if (ImpsTags.Polling_Request.equals(p.getType())) {
if (!mHasPendingPolling.compareAndSet(false, true)) {
ImpsLog.log("HttpDataChannel: Ignoring Polling-Request");
return;
}
} else if (ImpsTags.Logout_Request.equals(p.getType())) {
mStopRetry = true;
synchronized (mRetryLock) {
mRetryLock.notify();
}
}
if (!mSendQueue.offer(p)) {
// This is almost impossible for a LinkedBlockingQueue. We don't
// even bother to assign an error code for this. ;)
mTxManager.notifyErrorResponse(p.getTransactionID(),
ImErrorInfo.UNKNOWN_ERROR, "sending queue full");
}
}
@Override
public Primitive receivePrimitive() throws InterruptedException {
if (!mConnected || mStopped) {
throw new IllegalStateException();
}
return mReceiveQueue.take();
}
@Override
public void startKeepAlive(long interval) {
if (!mConnected || mStopped) {
throw new IllegalStateException();
}
if (interval <= 0) {
interval = mConnection.getConfig().getDefaultKeepAliveInterval();
}
mKeepAliveMillis = interval * 1000;
if (mKeepAliveMillis < 0) {
ImpsLog.log("Negative keep alive time. Won't send keep-alive");
}
mKeepAlivePrimitive = new Primitive(ImpsTags.KeepAlive_Request);
startHeartbeat();
}
private void startHeartbeat() {
HeartbeatService heartbeatService
= SystemService.getDefault().getHeartbeatService();
if (heartbeatService != null) {
heartbeatService.startHeartbeat(this, mKeepAliveMillis);
}
}
public long sendHeartbeat() {
if (mSuspended) {
return 0;
}
long inactiveTime = SystemClock.elapsedRealtime() - mLastActive;
if (needSendKeepAlive(inactiveTime)) {
sendKeepAlive();
return mKeepAliveMillis;
} else {
return mKeepAliveMillis - inactiveTime;
}
}
private boolean needSendKeepAlive(long inactiveTime) {
return mKeepAliveMillis - inactiveTime <= 500;
}
@Override
public long getLastActiveTime() {
return mLastActive;
}
@Override
public boolean isSendingQueueEmpty() {
if (!mConnected || mStopped) {
throw new IllegalStateException();
}
return mSendQueue.isEmpty();
}
public void run() {
while (!mStopped) {
try {
Primitive primitive = mSendQueue.take();
if (primitive.getType().equals(ImpsTags.Polling_Request)) {
mHasPendingPolling.set(false);
}
doSendPrimitive(primitive);
} catch (InterruptedException e) {
}
}
mHttpClient.close();
}
private void sendKeepAlive() {
ImpsTransactionManager tm = mConnection.getTransactionManager();
AsyncTransaction tx = new AsyncTransaction(tm) {
@Override
public void onResponseError(ImpsErrorInfo error) {
}
@Override
public void onResponseOk(Primitive response) {
// Since we never request a new timeout value, the response
// can be ignored
}
};
tx.sendRequest(mKeepAlivePrimitive);
}
/**
* Sends a primitive to the IMPS server through HTTP.
*
* @param p The primitive to send.
*/
private void doSendPrimitive(Primitive p) {
String errorInfo = null;
int retryCount = 0;
long retryDelay = INIT_RETRY_DELAY_MS;
while (retryCount < MAX_RETRY_COUNT) {
try {
trySend(p);
return;
} catch (IOException e) {
errorInfo = e.getLocalizedMessage();
String type = p.getType();
if (ImpsTags.Login_Request.equals(type)
|| ImpsTags.Logout_Request.equals(type)) {
// we don't retry to send login/logout request. The request
// might be sent to the server successfully but we failed to
// get the response from the server. Retry in this case might
// cause multiple login which is not allowed by some server.
break;
}
if (p.getTransactionMode() == TransactionMode.Response) {
// Ignore the failure of sending response to the server since
// it's only an acknowledgment. When we get here, the
// primitive might have been sent successfully but failed to
// get the http response. The server might or might not send
// the request again if it does not receive the acknowledgment,
// the client is ok to either case.
return;
}
retryCount++;
// sleep for a while and retry to send the primitive in a new
// transaction if we havn't met the max retry count.
if (retryCount < MAX_RETRY_COUNT) {
mTxManager.reassignTransactionId(p);
Log.w(ImpsLog.TAG, "Send primitive failed, retry after " + retryDelay + "ms");
synchronized (mRetryLock) {
try {
mRetryLock.wait(retryDelay);
} catch (InterruptedException ignore) {
}
if (mStopRetry) {
break;
}
}
retryDelay = retryDelay * 2;
if (retryDelay > MAX_RETRY_DELAY_MS) {
retryDelay = MAX_RETRY_DELAY_MS;
}
}
}
}
Log.w(ImpsLog.TAG, "Failed to send primitive after " + MAX_RETRY_COUNT + " retries");
mTxManager.notifyErrorResponse(p.getTransactionID(),
ImErrorInfo.NETWORK_ERROR, errorInfo);
}
private void trySend(Primitive p) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
mSerializer.serialize(p, out);
} catch (SerializerException e) {
mTxManager.notifyErrorResponse(p.getTransactionID(),
ImErrorInfo.SERIALIZER_ERROR,
"Internal serializer error, primitive: " + p.getType());
out.close();
return;
}
HttpPost req = new HttpPost(mPostUri);
req.addHeader(mContentTypeHeader);
if (mMsisdnHeader != null) {
req.addHeader(mMsisdnHeader);
}
ByteArrayEntity entity = new ByteArrayEntity(out.toByteArray());
req.setEntity(entity);
mLastActive = SystemClock.elapsedRealtime();
if (Log.isLoggable(ImpsLog.TAG, Log.DEBUG)) {
long sendBytes = entity.getContentLength() + 176 /* approx. header length */;
ImpsLog.log(mConnection.getLoginUserName() + " >> " + p.getType() + " HTTP payload approx. " + sendBytes + " bytes");
}
if (Log.isLoggable(ImpsLog.PACKET_TAG, Log.DEBUG)) {
ImpsLog.dumpRawPacket(out.toByteArray());
ImpsLog.dumpPrimitive(p);
}
HttpResponse res = mHttpClient.execute(req);
StatusLine statusLine = res.getStatusLine();
HttpEntity resEntity = res.getEntity();
InputStream in = resEntity.getContent();
if (Log.isLoggable(ImpsLog.PACKET_TAG, Log.DEBUG)) {
Log.d(ImpsLog.PACKET_TAG, statusLine.toString());
Header[] headers = res.getAllHeaders();
for (Header h : headers) {
Log.d(ImpsLog.PACKET_TAG, h.toString());
}
int len = (int) resEntity.getContentLength();
if (len > 0) {
byte[] content = new byte[len];
int offset = 0;
int bytesRead = 0;
do {
bytesRead = in.read(content, offset, len);
offset += bytesRead;
len -= bytesRead;
} while (bytesRead > 0);
in.close();
ImpsLog.dumpRawPacket(content);
in = new ByteArrayInputStream(content);
}
}
try {
if (statusLine.getStatusCode() != HttpURLConnection.HTTP_OK) {
mTxManager.notifyErrorResponse(p.getTransactionID(), statusLine.getStatusCode(),
statusLine.getReasonPhrase());
return;
}
if (resEntity.getContentLength() == 0) {
// empty responses are only valid for Polling-Request or
// server initiated transactions
if ((p.getTransactionMode() != TransactionMode.Response)
&& !p.getType().equals(ImpsTags.Polling_Request)) {
mTxManager.notifyErrorResponse(p.getTransactionID(),
ImErrorInfo.ILLEGAL_SERVER_RESPONSE,
"bad response from server");
}
return;
}
Primitive response = mParser.parse(in);
if (Log.isLoggable(ImpsLog.PACKET_TAG, Log.DEBUG)) {
ImpsLog.dumpPrimitive(response);
}
if (Log.isLoggable(ImpsLog.TAG, Log.DEBUG)) {
long len = 2 + resEntity.getContentLength() + statusLine.toString().length() + 2;
Header[] headers = res.getAllHeaders();
for (Header header : headers) {
len += header.getName().length() + header.getValue().length() + 4;
}
ImpsLog.log(mConnection.getLoginUserName() + " << "
+ response.getType() + " HTTP payload approx. " + len + "bytes");
}
if (!mReceiveQueue.offer(response)) {
// This is almost impossible for a LinkedBlockingQueue.
// We don't even bother to assign an error code for it.
mTxManager.notifyErrorResponse(p.getTransactionID(),
ImErrorInfo.UNKNOWN_ERROR, "receiving queue full");
}
} catch (ParserException e) {
ImpsLog.logError(e);
mTxManager.notifyErrorResponse(p.getTransactionID(),
ImErrorInfo.PARSER_ERROR,
"Parser error, received a bad response from server");
} finally {
//consume all the content so that the connection can be re-used.
resEntity.consumeContent();
}
}
}