package com.aberdyne.droidnavi.client;
import java.util.Stack;
import java.util.concurrent.CopyOnWriteArraySet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.content.Context;
import com.aberdyne.droidnavi.client.ServerListManager.ServerListListener;
import pctelelog.events.AbstractEvent;
import pctelelog.events.MissedCallEvent;
public class EventDispatchThread extends Thread implements ServerListListener {
/* Logger */
private static final Logger logger = LoggerFactory.getLogger(EventDispatchThread.class);
/*
* Server connection sets
* m_connectedServers: servers currently connected
* m_standbyServers: servers that are not connected and cannot receive events
*/
private CopyOnWriteArraySet<ServerConnection> m_connectedServers =
new CopyOnWriteArraySet<ServerConnection>();
private CopyOnWriteArraySet<ServerConnection> m_standbyServers =
new CopyOnWriteArraySet<ServerConnection>();
/*
* Event queue
*/
private Stack<AbstractEvent> m_queue = new Stack<AbstractEvent>();
/* Thread Variables */
private volatile boolean isStop = false;
private long SLEEP_TIMEOUT = 5 * 1000; // 5 second
/* Application Variables */
private Context m_context = null;
/*
* Sends the events and helps decide between multicast or direct-server
*/
private NetworkDispatch m_networkDispatch = null;
public EventDispatchThread(Context context) {
if(context == null) {
throw new NullPointerException();
}
m_context = context;
m_networkDispatch = new NetworkDispatch(m_context);
ServerListManager.addServerListListener(this);
ServerListManager.getSync(m_context);
}
@Override
public void run() {
logger.trace("ENTRY EventDispatchThread.run");
CheckServerThread checkServerThread = new CheckServerThread(this);
while(!isStop) {
if(!m_networkDispatch.hasMulticast()) {
// Check standby servers if we aren't currently
if(! checkServerThread.isAlive()) {
checkServerThread = new CheckServerThread(this);
checkServerThread.start();
}
}
if(m_queue.size() == 0 || !canDispatchToNetwork()) {
try {
sleep(SLEEP_TIMEOUT);
} catch(InterruptedException e) {}
continue;
}
// Get an event
AbstractEvent event = dequeueEvent();
if(event == null) {
continue;
}
// Dispatch event
if(m_networkDispatch.hasMulticast()) {
boolean result = m_networkDispatch.sendEvent(event);
if(result) {
logger.debug("Event dispatch success (multi): " + event.getEventType().toString());
}
else {
logger.debug("Event dispatch fail (multi): " + event.getEventType().toString());
}
}
else {
/*
* Attempt to send to multicast relay server.
* Loop will break on first success, no need to spam the network
* with multiple multicasts.
*/
for(ServerConnection server : m_connectedServers) {
boolean result = m_networkDispatch.sendEvent(event, server);
if(result == false) {
logger.debug("Event dispatch fail (tcp): " + event.getEventType().toString());
// Move to standby
removeConnectedServer(server);
addStandByServer(server);
// Let others know of the change
ServerListManager.updateServer(m_context, server);
}
else {
logger.debug("Event dispatch success (tcp): " + event.getEventType().toString());
break;
}
}
}
}
if(checkServerThread != null && checkServerThread.isAlive()) {
checkServerThread.quit();
}
setAllToStandby();
ServerListManager.removeServerListListener(this);
logger.trace("EXIT EventDispatchThread.run");
}
/**
* Set the thread to exit.
*
* Any existing events in queue will NOT be sent
*/
public void quit() {
isStop = true;
this.interrupt();
}
/**
* Dispatch an event to servers.
*
* Duplicate events won't be added.
*
* Call events will not be dispatched if there are no currently
* connected servers.
*
* Unread Missed calls will however be added to the queue regardless
* of connected servers and will be dispatched once servers connect.
*
* @param event An event to dispatch to waiting servers.
*/
public synchronized void dispatchEvent(AbstractEvent event) {
if(canDispatchToNetwork() && !m_queue.contains(event)) {
logger.info("Event added to dispatch queue: {}", event);
m_queue.add(event);
this.interrupt();
}
else if((event instanceof MissedCallEvent) && !m_queue.contains(event)) {
logger.info("Unread Missed Call Event added to dispatch queue: {}", event);
m_queue.add(event);
}
else {
int size = -1;
if(m_connectedServers != null) {
size = m_connectedServers.size();
}
logger.debug("Event not dispatched. Connected: " + Integer.toString(size) +
" Contains: " + Boolean.toString(m_queue.contains(event)));
}
}
/**
* Global Server List Changed
*
* In instances where REMOVE is the action, the supplied ServerConnection will not be
* a valid connected socket and cannot be used directly to disconnect the server.
* An equals() should be called on every server to compare the two objects and find
* the currently active server to remove.
*
* @param action An action event that took place
* @param server A server connection related to the action.
*/
public void onServerListChange(Action action, final ServerConnection server) {
logger.debug("EventDspatch:ServerList Event: {} {}", action, server);
switch(action) {
case SYNC:
case ADD:
if(server.getStandbyStatus())
addStandByServer(server);
else
addConnectedServer(server);
break;
case REMOVE:
if(m_connectedServers.contains(server)) { // Connected servers must be shutdown() first
for(ServerConnection connection : m_connectedServers) {
if(connection.equals(server)) {
connection.shutdown(true);
break;
}
}
removeConnectedServer(server);
}
else if(removeStandByServer(server));
else {
logger.error("Failed to remove: {}", server.toString());
}
break;
default:
break; // This class issues UPDATEs, action already handled.
}
this.interrupt();
}
/**
* Check if an event could be dispatched right now.
*
* If multicast is available then this always returns true.
* May return true or false depending on current server connections.
*
* @return True if a message could be dispatch. False if not.
*/
private boolean canDispatchToNetwork() {
if(m_networkDispatch.hasMulticast()) {
return true;
}
else if(m_connectedServers.size() > 0) {
return true;
}
return false;
}
/**
* Shutdown all servers and move to standby
*
* Called as thread is exiting run()
*/
private void setAllToStandby() {
logger.trace("ENTRY EventDispatchThread.setAllToStandby");
for(ServerConnection server : m_connectedServers) {
server.shutdown(true); // Shutdown and send ShutdownEvent
removeConnectedServer(server);
addStandByServer(server);
ServerListManager.updateServer(m_context, server);
}
logger.trace("ENTRY EventDispatchThread.setAllToStandby");
}
/**
* Adds a server to the standby set.
*
* Servers on standby are not connected but will be checked occasionally
* to try and reconnect to them.
*
* This method will not verify connected state.
* @param server A server that isn't connected
* @return True if the server was added. False if it wasn't.
*/
private boolean addStandByServer(ServerConnection server) {
if(m_standbyServers.add(server)) {
logger.info("Set Standby: " + server.toString());
return true;
}
return false;
}
/**
* Removes a server from the standby set.
*
* Servers being removed SHOULD be connected.
*
* This method will not verify connected state.
* @param server A connected server
* @return True if a server was removed. False if it wasn't.
*/
private boolean removeStandByServer(ServerConnection server) {
if(m_standbyServers.remove(server)) {
logger.info("UnSet Standby: " + server.toString());
return true;
}
return false;
}
/**
* Add a server to the connected set putting it into the event dispatch
* rotation.
*
* Servers added to this list should be connected and part of the global
* server list.
*
* This method will not verify connected state.
* @param server
* @return True if the server was added. False if it wasn't.
*/
private synchronized boolean addConnectedServer(ServerConnection server) {
if(m_connectedServers.add(server)) {
this.interrupt();
logger.info("Active Server added: " + server.toString());
return true;
}
return false;
}
/**
* Remove a server from the connected set, removing it from the event
* dispatch rotation.
*
* Servers removed should either be on standby or being removed from the
* global list.
* @param server
* @return
*/
private synchronized boolean removeConnectedServer(ServerConnection server) {
if(m_connectedServers.remove(server)) {
logger.info("Active Server removed: " + server.toString());
return true;
}
return false;
}
private AbstractEvent dequeueEvent() {
AbstractEvent event;
try {
event = m_queue.remove(0);
} catch(ArrayIndexOutOfBoundsException e) {
event = null;
}
return event;
}
/**
* A small thread to check the servers to see
* if they are active or can be connected to now.
*
* Due to the blocking nature of connect() testing for connection
* needs to be done inside a thread otherwise it can interfere
* with message dispatch responsiveness.
*
* @author Jeremy May
*
*/
private class CheckServerThread extends Thread {
private Thread m_parent = null;
private volatile boolean isStop = false;
public CheckServerThread(Thread parent) {
m_parent = parent;
}
@Override
public void run() {
checkServers();
// Interrupt dispatch, it could be sleeping.
if(m_parent != null && m_parent.isAlive()) {
m_parent.interrupt();
}
}
public void quit() {
isStop = true;
this.interrupt();
}
/**
* Check servers on standby to see if they have come online.
* Move the server from standby to connected.
*/
private void checkServers() {
logger.debug("Checking active servers");
for(ServerConnection server : m_connectedServers) {
// Move connected servers that fail heartbeat, let App know
if(!server.heartbeat()) {
removeConnectedServer(server);
addStandByServer(server);
ServerListManager.updateServer(m_context, server);
}
}
logger.debug("Checking standby servers.");
for(ServerConnection server : m_standbyServers) {
if(isStop) break;
boolean result = server.connect();
if(isStop) break;
// Swap standby to connected, let App know
if(result == true) {
removeStandByServer(server);
addConnectedServer(server);
ServerListManager.updateServer(m_context, server);
}
}
}
};
}