package pl.szpadel.android.gadu;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import pl.szpadel.android.gadu.packets.BasePacket;
import pl.szpadel.android.gadu.packets.ReceivedPacket;
import pl.szpadel.android.gadu.packets.SendPacket;
import pl.szpadel.android.gadu.packets.ZeroLenghtSendPacket;
/// Connection thread. Responsible for connection management, network communication,
/// receiving messages from UI thread and sending received packets and connection status to UI thread
public class CommunicationThread extends Thread {
/////////////////////////////////////////////////////////
// data
/// Parent service
CommunicationService mService;
/// Connector
private Connector mConnector;
/// flag - if reconnection should be attempted
private boolean mReconnect = true;
// flag - stop working.
//private boolean mQuit = false;
/// comm channel
//SocketChannel mChannel;
/// Inter-thread message queue
/// connection thread is the receiver, other threads are feeding into the queue
private BlockingQueue<InterThreadMessage> mInterthreadQueue = new LinkedBlockingQueue<InterThreadMessage>();
//////////////////////////////////////////////////////////
// types
/// Thread message type. USed for inter-thread communication
private static class InterThreadMessage {
/// message type
public int type;
// known types
// commands from ui thread
public static final int TYPE_RECONECT_NOW = 0; // command - reconnect now
public static final int TYPE_DISCONNECT = 1; // command - disconnect
public static final int TYPE_SEND_PACKET = 2; // command - send packet
public static final int TYPE_QUIT = 3; // stops the comm thread
// info from reading thread
public static final int TYPE_DISCONNECTED = 100; // info - connection broken
/// general purpose fields
public SendPacket sendPacket;
public InterThreadMessage(int t) {
type = t;
}
}
////////////////////////////////
// Receiving thread type
private class ReceivingThread extends Thread {
/// channel to read from
private ReadableByteChannel mChannel;
/// c-tor
ReceivingThread(ReadableByteChannel channel) {
super();
assert(channel != null);
mChannel = channel;
}
/// the routine
public void run() {
try {
while(true) {
AGLog.d(CommunicationService.TAG, "rth: receiving packet");
ReceivedPacket packet = ReceivedPacket.createFromChannel(mChannel);
AGLog.i(CommunicationService.TAG, "rth: received packet: " + packet);
handlePacket(packet);
}
} catch (ClosedChannelException e) {
AGLog.i(CommunicationService.TAG, "rth:channel closed, ending");
} catch(Exception e) {
String msg = "rth:comm error, sending itm and ending: exception:" + e.toString() + ", \nmsg:";
for (StackTraceElement st : e.getStackTrace()) {
msg += "\n > " + st;
}
AGLog.i(CommunicationService.TAG, msg);
mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_DISCONNECTED));
}
}
}
/////////////////////////////////////////////////
// intereface
/// C-tor
public CommunicationThread(CommunicationService service) {
super("connection thread");
mService = service;
mConnector = new Connector(mService);
}
/// Routine
public void run() {
// try connecting
mReconnect = false; // wait for command
SocketChannel channel = null;
ReceivingThread receivingThread = null;
long lastPingTime = 0;
boolean quit = false;
try {
do {
AGLog.v(CommunicationService.TAG, "th:main loop. channel is null:" + (channel==null) + ", reconnect=" + mReconnect);
if (mReconnect && channel == null) {
channel = tryConnecting();
receivingThread = null;
if (channel != null) {
try {
AGLog.i(CommunicationService.TAG, "th: connected, starting receiving channel");
receivingThread = new ReceivingThread(channel);
receivingThread.start();
// connected now
sendConnectionState(ConnectionState.Connected());
lastPingTime = System.currentTimeMillis();
} catch (Exception e) {
AGLog.e(CommunicationService.TAG, "th: failed to start rerading thread:"+e.getMessage());
channel = null; // will try again...
}
} else {
AGLog.e(CommunicationService.TAG, "failed to connect");
}
}
// wait for events
AGLog.v(CommunicationService.TAG, "waiting for itm");
try {
// exit after 10s to i.e. to retry reconnecting
InterThreadMessage itm = mInterthreadQueue.poll(60, TimeUnit.SECONDS);
// any itms?
if (itm != null){
AGLog.v(CommunicationService.TAG, "handling itm of type=" + itm.type);
switch(itm.type) {
case InterThreadMessage.TYPE_QUIT:
quit = true;
break;
case InterThreadMessage.TYPE_RECONECT_NOW:
closeChannelAndJoin(channel, receivingThread);
sendConnectionState(ConnectionState.Connecting());
channel = null;
mReconnect = true;
break;
case InterThreadMessage.TYPE_DISCONNECT:
closeChannelAndJoin(channel, receivingThread);
sendConnectionState(ConnectionState.Disconnected(ConnectionState.REASON_DISCONNECTED_BY_USER));
channel = null;
mReconnect = false;
break;
case InterThreadMessage.TYPE_DISCONNECTED:
closeChannelAndJoin(channel, receivingThread);
channel = null;
mReconnect = true;
break;
case InterThreadMessage.TYPE_SEND_PACKET:
// send only if channel connected
try {
if (channel != null) {
assert(itm.sendPacket != null);
itm.sendPacket.toChannel(channel);
AGLog.i(CommunicationService.TAG, "th: Sending packet: " + itm.sendPacket);
}
} catch (Exception e ) {
AGLog.e(CommunicationService.TAG, "th: failed to send packet " + itm.sendPacket);
// actual error will be handled in receiving thread
}
default:
; // do nothing
} // switch
} // if itm
// ping time?
if (channel != null) {
long timeFromLastPing = System.currentTimeMillis() - lastPingTime;
if (timeFromLastPing >= 4*60*1000) { // 4 minutes
try {
AGLog.v(CommunicationService.TAG, "th: sending PING");
ZeroLenghtSendPacket ping = new ZeroLenghtSendPacket(BasePacket.TYPE_PING);
ping.toChannel(channel);
lastPingTime = System.currentTimeMillis();
} catch (IOException e ) {
AGLog.e(CommunicationService.TAG, "th: error sending ping: " + e.getMessage() );
// errors are handled by reading thread
}
}
}
} catch (Exception e) {
AGLog.e(CommunicationService.TAG, "th:Error reading/handling itm: " + e.getMessage());
}
} while(quit==false);
} catch(Exception e) {
AGLog.e(CommunicationService.TAG, "th: unhandled exception in main loop");
}
AGLog.i(CommunicationService.TAG, "th: thread exists");
// wait for receiving thread
closeChannelAndJoin(channel, receivingThread);
}
/// Stops and joins the thread
/// call from ui thread
public void stopCommThread() {
AGLog.i(CommunicationService.TAG, "th:stopCommThread()");
mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_QUIT));
try {
join();
} catch (InterruptedException e) {
; // nothing
}
}
/// Forces to reconnect to server ASAP
public void forceReconnect() {
AGLog.i(CommunicationService.TAG, "th:forceReconnect()");
mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_RECONECT_NOW));
}
/// Disconnects
public void disconnect() {
AGLog.i(CommunicationService.TAG, "th:disconnect()");
mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_DISCONNECT));
}
/// Sends packet (if connected)
public void sendPacket(SendPacket packet) {
InterThreadMessage itm = new InterThreadMessage(InterThreadMessage.TYPE_SEND_PACKET);
itm.sendPacket = packet;
mInterthreadQueue.offer(itm);
}
////////////////////////////////////////////////////
// implementation
/// Handles received packet
private void handlePacket(final ReceivedPacket packet) {
// simply send yo ui thread
AGLog.i(CommunicationService.TAG, "th: sending packet to ui thread: " + packet);
mService.sendToUiThread(new Runnable(){
public void run() { mService.onPacket(packet); }
});
}
/// Closes channel, joins thread
static private void closeChannelAndJoin(SocketChannel channel, Thread t) {
try {
if (channel != null) {
AGLog.d(CommunicationService.TAG, "th: closing channel...");
channel.close();
AGLog.d(CommunicationService.TAG, "th: channel closed");
}
if (t != null) {
AGLog.d(CommunicationService.TAG, "th: joining thread...");
t.interrupt(); // TODO this is hack, i HATE it!
t.join(1000); // 1 second timeout
if (t.isAlive()) {
AGLog.e(CommunicationService.TAG, "th: failed to join thread");
}
AGLog.d(CommunicationService.TAG, "th: thread joined");
}
} catch (Exception e) {
AGLog.e(CommunicationService.TAG, "th: error closing channel: " + e);
; // do nothing
}
}
// Tries connecting. Sets status accordingly
// returns channel if connected, null if not
private SocketChannel tryConnecting() {
sendConnectionState(ConnectionState.Connecting());
// read uin and pwd
Config config = App.getInstance().getConfig();
if (config.hasLoginDetails()) {
long uin = config.getUin();
String password = config.getPassword();
// try connecting
try {
AGLog.i(CommunicationService.TAG, "th:tryConnect: connecting with uin=" + uin);
return mConnector.connect(uin, password, config.getStatus());
} catch (Connector.LoginException e) {
mReconnect = false;
sendConnectionState(ConnectionState.Disconnected(ConnectionState.REASON_LOGIN_FAILED));
AGLog.e(CommunicationService.TAG, "th:tryConnect: login failed: " + e.getMessage());
return null;
} catch (Connector.NetworkException e) {
mReconnect = true; // will try to reconnect after some time
AGLog.e(CommunicationService.TAG, "th:tryConnect: network error: " + e.getMessage());
return null;
} catch (Connector.ProtocolException e) {
mReconnect = true; // will try to reconnect after some time
AGLog.e(CommunicationService.TAG, "th:tryConnect: protocol error: " + e.getMessage());
return null;
} catch (Connector.RejectException e) {
mReconnect = false; // will try to reconnect after some time
AGLog.e(CommunicationService.TAG, "th:tryConnect: connection rejected: " + e.getMessage());
return null;
} catch (Connector.ConnectorException e) {
; // java sux
}
}
return null;
}
// sends connection state to UI thread to service
private void sendConnectionState(final ConnectionState state) {
mService.sendToUiThread(new Runnable() {
public void run() { mService.onConnectionStateChange(state); }
});
}
}
|