/*
* 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.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Hashtable;
import android.content.Context;
import android.os.PowerManager;
import com.vodafone360.people.Settings;
import com.vodafone360.people.datatypes.AuthSessionHolder;
import com.vodafone360.people.engine.EngineManager;
import com.vodafone360.people.engine.login.LoginEngine;
import com.vodafone360.people.service.RemoteService;
import com.vodafone360.people.service.transport.ConnectionManager;
import com.vodafone360.people.service.transport.IWakeupListener;
import com.vodafone360.people.service.transport.http.HttpConnectionThread;
import com.vodafone360.people.service.utils.AuthUtils;
import com.vodafone360.people.service.utils.hessian.HessianEncoder;
import com.vodafone360.people.service.utils.hessian.HessianUtils;
import com.vodafone360.people.service.io.rpg.RpgHeader;
import com.vodafone360.people.service.io.rpg.RpgMessageTypes;
/**
* Sends heartbeats to the RPG in order to keep the connection alive.
*
* @author Rudy Norff (rudy.norff@vodafone.com)
*/
public class HeartbeatSenderThread implements Runnable, IWakeupListener {
/**
* The HeartbeatSenderThread that was created by the TcpConnectionThread. It
* will be compared to this thread. If they differ this thread must be a
* lock thread that got stuck when the user changed APNs or switched from
* WiFi to another data connection type.
*/
protected static HeartbeatSenderThread mCurrentThread;
/**
* The milliseconds in a second.
*/
private static final long MILLIS_PER_SEC = 1000;
/**
* The service under which context the hb sender runs in. Used for setting
* alarms that wake up the CPU for sending out heartbeats.
*/
private RemoteService mService;
/**
* The managing thread that needs to be called back if an IOException occurs
* sending a heartbeat.
*/
private TcpConnectionThread mConnThread;
/**
* The thread continuously sending the heartbeats.
*/
protected Thread mThread;
/**
* The output stream to write the heartbeat to.
*/
protected OutputStream mOs;
/**
* This is the interval at which the heartbeat gets sent. The problem with
* this is that this timeout interval is only proven in the Vodafone
* network. In other networks we will do an autodetect for the correct
* interval settings.
*/
protected long mHeartbeatInterval;
/**
* Indicates that the connection is running if true.
*/
private boolean mIsConnectionRunning;
/**
* Keeps a partial wake lock on the CPU that will prevent it from sleeping
* and allow the Sender to send off heartbeats.
*/
private PowerManager.WakeLock mWakeLock;
/**
* The socket to write to.
*/
private Socket mSocket;
/**
* Constructs a heartbeat-sender and passes the connection thread to call
* back to in case of errors.
*
* @param connThread The connection thread to call back to in case of
* networking issues.
* @param service The remote service that we register with once we have set
* the heartbeat alarm.
*/
public HeartbeatSenderThread(TcpConnectionThread connThread, RemoteService service,
Socket socket) {
mConnThread = connThread;
mHeartbeatInterval = Settings.TCP_VF_HEARTBEAT_INTERVAL;
mService = service;
mSocket = socket;
if (null != mService) {
mService.registerCpuWakeupListener(this);
PowerManager pm = (PowerManager)mService.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"HeartbeatSenderThread.HeartbeatSenderThread()");
}
}
/**
* Sets the state of the connection to run and spawns a thread to run it in.
*/
public synchronized void startConnection() {
HttpConnectionThread.logI("RpgTcpHeartbeatSender.startConnection()", "STARTING HB Sender!");
mIsConnectionRunning = true;
mThread = null; // if we are restarting let's clear the last traces
mThread = new Thread(this, "RpgTcpHeartbeatSender");
mThread.start();
}
/**
* Stops the heartbeat-senders connection and closes the output-stream to
* the socket-connection.
*/
public synchronized void stopConnection() {
mIsConnectionRunning = false;
synchronized (this) {
notify();
}
mService.setAlarm(false, 0);
if (null != mSocket) {
try {
mSocket.shutdownOutput();
} catch (IOException ioe) {
HttpConnectionThread.logE("RpgTcpHeartbeatSender.stopConnection()",
"Could not shutdown OutputStream", ioe);
} finally {
mSocket = null;
}
}
if (null != mOs) {
try {
mOs.close();
} catch (IOException ioe) {
HttpConnectionThread.logE("RpgTcpHeartbeatSender.stopConnection()",
"Could not close OutputStream", ioe);
} finally {
mOs = null;
}
}
}
/**
* Sets the output-stream so that the heartbeat-sender can send its
* heartbeats to the RPG.
*
* @param outputStream The open output-stream that the heartbeats shall be
* sent to. Avoid passing null as this will result in an
* error-callback to RpgTcpConnectionThread which will completely
* reestablish the socket connection.
*/
public void setOutputStream(OutputStream outputStream) {
HttpConnectionThread.logI("RpgTcpHeartbeatSender.setOutputStream()", "Setting new OS.");
mOs = outputStream;
}
/**
* The run-method overriding Thread.run(). The method continuously sends out
* heartbeats by calling sendHeartbeat() and then waits for a
* Thread.interrupt(), a notify or for the wait timer to time out after a
* timer-value defined in Settings.TCP_VF_HEARTBEAT_INTERVAL.
*/
public void run() {
// sets the wakeup alarm at XX minutes. This is important as the CPU
// gets woken
// by this call if it should be in standby mode.
while (mIsConnectionRunning) {
try {
if (!this.equals(mCurrentThread)) {
// The current thread created by the TcpConnectionThread is
// not equal to this thread.
// This thread must be old (locked due to the user changing
// his network settings.
HttpConnectionThread.logE("HeartbeatSenderThread.run()", "This thread is a "
+ "locked thread caused by the connection settings being switched.",
null);
stopConnection(); // exit this thread
break;
}
mWakeLock.acquire();
sendHeartbeat();
mService.setAlarm(true, (System.currentTimeMillis() + mHeartbeatInterval));
} catch (IOException ioe) {
if ((null != mConnThread) && (mIsConnectionRunning)) {
mConnThread.notifyOfNetworkProblems();
}
HttpConnectionThread.logE("RpgTcpHeartbeatSender.run()", "Could not send HB!", ioe);
} catch (Throwable t) {
if ((null != mConnThread) && (mIsConnectionRunning)) {
mConnThread.notifyOfNetworkProblems();
}
HttpConnectionThread.logE("RpgTcpHeartbeatSender.run()",
"Could not send HB! Unknown: ", t);
} finally {
if (mWakeLock.isHeld()) {
mWakeLock.release();
}
}
synchronized (this) {
try {
wait(mHeartbeatInterval + (mHeartbeatInterval / 5));
} catch (InterruptedException e) {
HttpConnectionThread.logE("RpgTcpHeartbeatSender.run()", "Failed sleeping", e);
}
}
}
}
/**
* Prepares the necessary Hessian payload and writes it directly to the open
* output-stream of the socket.
*
* @throws Exception Thrown if there was an unknown problem writing to the
* output-stream.
* @throws IOException Thrown if there was a problem regarding IO while
* writing to the output-stream.
*/
public void sendHeartbeat() throws IOException, Exception {
byte[] rpgMsg = null;
try {
rpgMsg = getHeartbeatHessianPayload();
} catch(NullPointerException e) {
// Stop connnection and log out
ConnectionManager.getInstance().onLoginStateChanged(false);
EngineManager.getInstance().getLoginEngine().logoutAndRemoveUser();
}
try {
// Try and issue the request
if (Settings.sEnableProtocolTrace) {
Long userID = null;
AuthSessionHolder auth = LoginEngine.getSession();
if (auth != null) {
userID = auth.userID;
}
HttpConnectionThread.logI("RpgTcpHeartbeatSender.sendHeartbeat()",
"\n * Sending a heartbeat for user ID "
+ userID + "----------------------------------------"
+ HessianUtils.getInHessian(new ByteArrayInputStream(rpgMsg), true)
+ "\n");
}
if (null != mOs) {
synchronized (mOs) {
mOs.write(rpgMsg);
mOs.flush();
}
}
} catch (IOException ioe) {
HttpConnectionThread.logE("RpgTcpHeartbeatSender.sendHeartbeat()",
"Could not write HB to OS!", ioe);
throw ioe;
} catch (Exception e) {
HttpConnectionThread.logE("RpgTcpHeartbeatSender.sendHeartbeat()",
"Could not send HB to OS! Unknown: ", e);
throw e;
} finally {
rpgMsg = null;
}
}
/**
* Returns a byte-array containing the data needed for sending a heartbeat
* to the RPG.
*
* @throws IOException If there was an exception serializing the hash map to
* a hessian byte array.
* @return A byte array representing the heartbeat.
*/
private byte[] getHeartbeatHessianPayload() throws IOException {
// hash table for parameters to Hessian encode
final Hashtable<String, Object> ht = new Hashtable<String, Object>();
final String timestamp = "" + ((long)System.currentTimeMillis() / MILLIS_PER_SEC);
final AuthSessionHolder auth = LoginEngine.getSession();
if(auth == null) {
throw new NullPointerException();
}
ht.put("auth", AuthUtils
.calculateAuth("", new Hashtable<String, Object>(), timestamp, auth));
ht.put("userid", auth.userID);
// do Hessian encoding
final byte[] payload = HessianEncoder.createHessianByteArray("", ht);
payload[1] = (byte)1;
payload[2] = (byte)0;
final int reqLength = RpgHeader.HEADER_LENGTH + payload.length;
final RpgHeader rpgHeader = new RpgHeader();
rpgHeader.setPayloadLength(payload.length);
rpgHeader.setReqType(RpgMessageTypes.RPG_TCP_HEARTBEAT);
final byte[] rpgMsg = new byte[reqLength];
System.arraycopy(rpgHeader.createHeader(), 0, rpgMsg, 0, RpgHeader.HEADER_LENGTH);
if (null != payload) {
System.arraycopy(payload, 0, rpgMsg, RpgHeader.HEADER_LENGTH, payload.length);
}
return rpgMsg;
}
/**
* <p>
* Returns true if the heartbeat thread is currently running/active.
* </p>
* <p>
* Calling startConnection() will make this method return as the heartbeat
* thread gets started.
* </p>
* <p>
* stopConnection() will stop the connection again and make this method
* return false.
* </p>
*
* @return True if the connection is running, false otherwise.
*/
public boolean getIsActive() {
return mIsConnectionRunning;
}
@Override
public void notifyOfWakeupAlarm() {
if (Settings.sEnableProtocolTrace) {
HttpConnectionThread.logI("HeartbeatSenderThread.notifyOfWakeupAlarm()",
"Waking up for a heartbeat!");
}
synchronized (this) {
notify(); // this will try to send another heartbeat
}
}
}