summaryrefslogtreecommitdiff
path: root/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java
diff options
context:
space:
mode:
authorTreeki <treeki@gmail.com>2014-02-18 02:54:06 +0100
committerTreeki <treeki@gmail.com>2014-02-18 02:54:06 +0100
commitd57224505be243c80e9dd64e00f0a7c3a1d841f0 (patch)
treefe2b7d5cc9b7ca993b50f526f73bcb8716c0a60e /android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java
parent05568c427eff856d3049d4a18a707f1b0b358bd2 (diff)
downloadbounce4-d57224505be243c80e9dd64e00f0a7c3a1d841f0.tar.gz
bounce4-d57224505be243c80e9dd64e00f0a7c3a1d841f0.zip
add Android client
Diffstat (limited to 'android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java')
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java611
1 files changed, 611 insertions, 0 deletions
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java
new file mode 100644
index 0000000..5b6ac83
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java
@@ -0,0 +1,611 @@
+package net.brokenfox.vulpirc;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Created by ninji on 1/29/14.
+ */
+public class BaseConn {
+ // Listeners
+ public interface BaseConnListener {
+ void handleSessionStarted();
+ void handleSessionEnded();
+ void handleSocketStateChanged();
+ void handlePacketReceived(int type, byte[] data);
+ void handleLoginError(String error);
+ void handleStatusMessage(String message);
+ }
+
+ private BaseConnListener mListener = null;
+ public void setListener(BaseConnListener l) {
+ mListener = l;
+ }
+
+
+ // Internal communications
+ private final static int MSG_SESSION_STARTED = 100;
+ private final static int MSG_SESSION_ENDED = 101;
+ private final static int MSG_SOCKET_STATE_CHANGED = 102;
+ private final static int MSG_PACKET_RECEIVED = 103;
+ private final static int MSG_LOGIN_ERROR = 104;
+ private final static int MSG_STATUS_MESSAGE = 105;
+ private final static int MSG_TIMED_RECONNECT = 200;
+
+ private final Handler.Callback mHandlerCallback = new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SESSION_STARTED:
+ if (mListener != null)
+ mListener.handleSessionStarted();
+ return true;
+ case MSG_SESSION_ENDED:
+ if (mListener != null)
+ mListener.handleSessionEnded();
+ return true;
+ case MSG_SOCKET_STATE_CHANGED:
+ if (mListener != null)
+ mListener.handleSocketStateChanged();
+ return true;
+ case MSG_PACKET_RECEIVED:
+ if (mListener != null)
+ mListener.handlePacketReceived(message.arg1, (byte[]) message.obj);
+ return true;
+ case MSG_LOGIN_ERROR:
+ if (mListener != null)
+ mListener.handleLoginError((String)message.obj);
+ return true;
+ case MSG_STATUS_MESSAGE:
+ if (mListener != null)
+ mListener.handleStatusMessage((String)message.obj);
+ return true;
+ case MSG_TIMED_RECONNECT:
+ initiateConnection();
+ return true;
+ }
+ return false;
+ }
+ };
+ private final Handler mHandler = new Handler(mHandlerCallback);
+
+
+
+
+ // Packet system
+ public final static int PROTOCOL_VERSION = 1;
+ private final static int SESSION_KEY_SIZE = 16;
+
+ private static class Packet {
+ public int id;
+ public int type;
+ public byte[] data;
+ }
+
+ private final Object mPacketLock = new Object();
+
+ // Anything that touches the following fields must lock on
+ // mPacketLock first..!
+ private final ArrayList<Packet> mPacketCache =
+ new ArrayList<Packet>();
+
+ private byte[] mSessionKey = null;
+ private int mNextPacketID = 1;
+ private int mLastReceivedFromServer = 0;
+ private boolean mSessionActive = false;
+ private boolean mSessionWillTerminate = false;
+
+ public boolean getSessionActive() { return mSessionActive; }
+
+ // NOTE: This executes on the Socket thread, so locking
+ // is absolutely necessary!
+ private void processRawPacket(int packetType, int packetSize, int msgID, int lastReceivedByServer, byte[] buffer, int bufferPos) {
+ Log.i("VulpIRC", "Packet received: " + packetType + ", " + packetSize);
+
+ if ((packetType & 0x8000) == 0) {
+ // For in-band packets, handle the caching junk
+ synchronized (mPacketLock) {
+ clearCachedPackets(lastReceivedByServer);
+
+ if (msgID > mLastReceivedFromServer) {
+ // This is a new packet
+ mLastReceivedFromServer = msgID;
+ } else {
+ // We've already seen this packet, so ignore it
+ return;
+ }
+ }
+
+ // Fire off the packet to the Handler for further
+ // processing by a subclass
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_PACKET_RECEIVED,
+ packetType, 0,
+ Arrays.copyOfRange(buffer, bufferPos, bufferPos + packetSize)));
+ } else {
+ // Out-of-band packets are handled here.
+
+ synchronized (mPacketLock) {
+ switch (packetType) {
+ case 0x8001:
+ // Successful login
+ Log.i("VulpIRC", "*** Successful login. ***");
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Logged in successfully."));
+
+ mSessionKey = Arrays.copyOfRange(buffer, bufferPos, bufferPos + packetSize);
+ mLastReceivedFromServer = 0;
+ mNextPacketID = 1;
+ mPacketCache.clear();
+
+ if (mSessionActive)
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ mSessionActive = true;
+ mSessionWillTerminate = false;
+ mHandler.sendEmptyMessage(MSG_SESSION_STARTED);
+
+ break;
+
+ case 0x8002:
+ // Login failure. Output this to the UI somewhere?
+ Log.e("VulpIRC", "*** Login failed! ***");
+
+ ByteBuffer b = ByteBuffer.wrap(buffer);
+ b.order(ByteOrder.LITTLE_ENDIAN);
+ b.position(bufferPos);
+ int code = b.getInt();
+
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_LOGIN_ERROR,
+ "Login failure: " + code));
+
+ if (mSessionActive)
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ mSessionActive = false;
+
+ requestEndSession();
+
+ break;
+
+ case 0x8003:
+ // Session resumed.
+ Log.i("VulpIRC", "*** Session resumed. ***");
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Session resumed."));
+
+ lastReceivedByServer = buffer[bufferPos] |
+ (buffer[bufferPos + 1] << 8) |
+ (buffer[bufferPos + 2] << 16) |
+ (buffer[bufferPos + 3] << 24);
+
+ clearCachedPackets(lastReceivedByServer);
+ for (Packet p : mPacketCache)
+ sendPacketOverWire(p);
+ break;
+ }
+ }
+ }
+ }
+
+
+ // This function must only be called while mPacketLock is held!
+ private void clearCachedPackets(int maxID) {
+ while (!mPacketCache.isEmpty()) {
+ if (mPacketCache.get(0).id > maxID)
+ break;
+ else
+ mPacketCache.remove(0);
+ }
+ }
+
+
+ private void sendPacketOverWire(Packet p) {
+ int headerSize = ((p.type & 0x8000) == 0) ? 16 : 8;
+ ByteBuffer b = ByteBuffer.allocate(headerSize + p.data.length);
+
+ b.order(ByteOrder.LITTLE_ENDIAN);
+ b.putShort((short)p.type);
+ b.putShort((short)0);
+ b.putInt(p.data.length);
+
+ if ((p.type & 0x8000) == 0) {
+ b.putInt(p.id);
+ b.putInt(mLastReceivedFromServer);
+ }
+
+ b.put(p.data);
+
+ mWriteQueue.offer(b.array());
+ }
+
+
+ public void sendPacket(int type, byte[] data) {
+ Packet p = new Packet();
+ p.type = type;
+
+ if ((type & 0x8000) == 0) {
+ p.id = mNextPacketID;
+ ++mNextPacketID;
+ }
+
+ p.data = data;
+
+ synchronized (mPacketLock) {
+ if ((type & 0x8000) == 0)
+ mPacketCache.add(p);
+
+ synchronized (mSocketStateLock) {
+ if (mSocketState == SocketState.CONNECTED && mSessionActive)
+ sendPacketOverWire(p);
+ }
+ }
+ }
+
+
+ // Socket junk
+ private Socket mSocket = null;
+ private LinkedBlockingQueue<byte[]> mWriteQueue =
+ new LinkedBlockingQueue<byte[]>();
+
+
+ public enum SocketState {
+ DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING
+ }
+ // mSocketState can be accessed from multiple threads
+ // Always synchronise on the lock before doing anything with it!
+ private final Object mSocketStateLock = new Object();
+ private SocketState mSocketState = SocketState.DISCONNECTED;
+ public SocketState getSocketState() { return mSocketState; }
+
+ private String mHostname, mUsername, mPassword;
+ private int mPort;
+ private boolean mUseTls;
+
+
+ private class WriterThread implements Runnable {
+ private OutputStream mOutputStream = null;
+ public WriterThread(OutputStream os) {
+ mOutputStream = os;
+ }
+
+ @Override
+ public void run() {
+ // Should I be using a BufferedOutputStream?
+ // Not sure.
+
+ while (true) {
+ try {
+ byte[] data = mWriteQueue.take();
+ mOutputStream.write(data);
+ mOutputStream.flush();
+ } catch (IOException e) {
+ // oops
+ Log.e("VulpIRC", "WriterThread write failure:");
+ Log.e("VulpIRC", e.toString());
+ // Should disconnect here? Maybe?
+ break;
+ } catch (InterruptedException e) {
+ // nothing wrong here
+ break;
+ }
+ }
+ }
+ }
+ private Thread mSocketThread = null;
+
+ private class SocketThread implements Runnable {
+ @Override
+ public void run() {
+ synchronized (mSocketStateLock) {
+ mSocketState = SocketState.CONNECTING;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ Log.i("VulpIRC", "SocketThread running");
+
+ // No SSL just yet, let's simplify things for now
+
+ mSocket = new Socket();
+ try {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connecting..."));
+
+ mSocket.connect(new InetSocketAddress(mHostname, mPort));
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection failure (1): " + e.toString()));
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOGIN_ERROR, e.toString()));
+ cleanUpConnection();
+ return;
+ }
+
+ // We're connected, yay
+ Log.i("VulpIRC", "Connected!");
+
+ InputStream inputStream;
+ OutputStream outputStream;
+
+ try {
+ inputStream = mSocket.getInputStream();
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection failure (2): " + e.toString()));
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOGIN_ERROR, e.toString()));
+ cleanUpConnection();
+ return;
+ }
+
+ try {
+ outputStream = mSocket.getOutputStream();
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection failure (3): " + e.toString()));
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOGIN_ERROR, e.toString()));
+ cleanUpConnection();
+ return;
+ }
+
+ // Start up our writer
+ mWriteQueue.clear();
+
+ Thread writerThread = new Thread(new WriterThread(outputStream));
+ writerThread.start();
+
+ // Send the initial login packet
+ byte[] encPW = Util.encodeString(mPassword);
+
+ ByteBuffer loginData = ByteBuffer.allocate(12 + encPW.length + SESSION_KEY_SIZE);
+ loginData.order(ByteOrder.LITTLE_ENDIAN);
+ loginData.putInt(PROTOCOL_VERSION);
+ loginData.putInt(mLastReceivedFromServer);
+ loginData.putInt(encPW.length);
+ loginData.put(encPW);
+
+ if (mSessionKey == null) {
+ for (int i = 0; i < SESSION_KEY_SIZE; i++)
+ loginData.put((byte)0);
+ } else {
+ loginData.put(mSessionKey);
+ }
+
+ Packet loginPack = new Packet();
+ loginPack.type = 0x8001;
+ loginPack.data = loginData.array();
+ sendPacketOverWire(loginPack);
+
+ // Let's go !
+ synchronized (mSocketStateLock) {
+ mSocketState = SocketState.CONNECTED;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ Log.i("VulpIRC", "Beginning socket read");
+
+ byte[] readBuffer = new byte[16384];
+ int readBufSize = 0;
+
+ while (true) {
+ // Do we have enough space to (try and) read 8k?
+ // If not, make the buffer bigger
+ if ((readBufSize + 8192) > readBuffer.length)
+ readBuffer = Arrays.copyOf(readBuffer, readBuffer.length + 16384);
+
+ // OK, now we should have enough!
+ try {
+ int amount = inputStream.read(readBuffer, readBufSize, 8192);
+ readBufSize += amount;
+
+ if (amount == -1)
+ break;
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection lost (4): " + e.toString()));
+
+ break;
+ }
+
+ // Try and parse as many packets as we can!
+ int pos = 0;
+
+ while (true) {
+ // Do we have enough to parse a packet header?
+ if ((readBufSize - pos) < 8)
+ break;
+
+ int headerSize = 8;
+ int packetType = (0xFF & (int)readBuffer[pos]) |
+ ((0xFF & readBuffer[pos + 1]) << 8);
+ int packetSize = (0xFF & (int)readBuffer[pos + 4]) |
+ ((0xFF & (int)readBuffer[pos + 5]) << 8) |
+ ((0xFF & (int)readBuffer[pos + 6]) << 16) |
+ ((0xFF & (int)readBuffer[pos + 7]) << 24);
+
+ int msgID = 0, lastReceivedByServer = 0;
+
+ // In-band packets have extra stuff
+ if ((packetType & 0x8000) == 0) {
+ if ((readBufSize - pos) < 16)
+ break;
+
+ headerSize = 16;
+
+ msgID = (0xFF & (int)readBuffer[pos + 8]) |
+ ((0xFF & (int)readBuffer[pos + 9]) << 8) |
+ ((0xFF & (int)readBuffer[pos + 10]) << 16) |
+ ((0xFF & (int)readBuffer[pos + 11]) << 24);
+ lastReceivedByServer = (0xFF & (int)readBuffer[pos + 12]) |
+ ((0xFF & (int)readBuffer[pos + 13]) << 8) |
+ ((0xFF & (int)readBuffer[pos + 14]) << 16) |
+ ((0xFF & (int)readBuffer[pos + 15]) << 24);
+ }
+
+ // Negative packet sizes aren't right
+ if (packetSize < 0)
+ break;
+
+ // Enough data?
+ if ((readBufSize - pos) < (packetSize + headerSize))
+ break;
+
+ // OK, this should mean we can parse things now!
+ processRawPacket(
+ packetType, packetSize,
+ msgID, lastReceivedByServer,
+ readBuffer, pos + headerSize);
+
+ pos += headerSize + packetSize;
+ }
+
+ if (pos > 0) {
+ if (pos >= readBufSize) {
+ // We've read everything, no copying needed, just wipe it all
+ readBufSize = 0;
+ } else {
+ // Move the remainder to the beginning of the buffer
+ System.arraycopy(readBuffer, pos, readBuffer, 0, readBufSize - pos);
+ readBufSize -= pos;
+ }
+ }
+ }
+
+ synchronized (mSocketStateLock) {
+ mSocketState = SocketState.DISCONNECTING;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ // Clean up everything
+ writerThread.interrupt();
+ cleanUpConnection();
+ }
+
+ private void cleanUpConnection() {
+ if (mSocket != null) {
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ // don't care
+ }
+ mSocket = null;
+ }
+
+ synchronized (mSocketStateLock) {
+ synchronized (mPacketLock) {
+ if (mSessionWillTerminate) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Session completed."));
+
+ mSessionWillTerminate = false;
+ mSessionKey = null;
+ if (mSessionActive)
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ mSessionActive = false;
+ mHandler.removeMessages(MSG_TIMED_RECONNECT);
+ } else if (mSessionActive) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection closed. Reconnecting in 15 seconds."));
+ mHandler.sendEmptyMessageDelayed(MSG_TIMED_RECONNECT, 15 * 1000);
+ }
+ }
+
+ mSocketState = SocketState.DISCONNECTED;
+ mSocketThread = null;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ Log.i("VulpIRC", "Connection closed.");
+ }
+ }
+
+
+ public boolean initiateConnection(String hostname, int port, boolean useTls, String username, String password) {
+ synchronized (mSocketStateLock) {
+ if (mSocketState != SocketState.DISCONNECTED)
+ return false;
+
+ mHostname = hostname;
+ mPort = port;
+ mUseTls = useTls;
+ mUsername = username;
+ mPassword = password;
+
+ return initiateConnection();
+ }
+ }
+
+ public boolean initiateConnection() {
+ synchronized (mSocketStateLock) {
+ if (mSocketState != SocketState.DISCONNECTED)
+ return false;
+
+ mSocketState = SocketState.CONNECTING;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+
+ mSocketThread = new Thread(new SocketThread());
+ mSocketThread.start();
+
+ return true;
+ }
+ }
+
+ public boolean requestDisconnection() {
+ synchronized (mSocketStateLock) {
+ if (mSocketState != SocketState.DISCONNECTED) {
+ try {
+ if (mSocket != null)
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w("VulpIRC", "requestDisconnection could not close socket:");
+ Log.w("VulpIRC", e.toString());
+ }
+
+ mSocketThread.interrupt();
+ return true;
+ }
+ return false;
+ }
+ }
+ public boolean requestEndSession() {
+ // Should send an OOB logout packet to the server too, if possible.
+
+ synchronized (mSocketStateLock) {
+ mHandler.removeMessages(MSG_TIMED_RECONNECT);
+
+ synchronized (mPacketLock) {
+ if (mSocketState == SocketState.DISCONNECTED) {
+ if (mSessionActive) {
+ mSessionActive = false;
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ }
+ return true;
+ } else {
+ mSessionWillTerminate = true;
+ return requestDisconnection();
+ }
+ }
+ }
+ }
+}