/*
SubspaceMobile - A subspace/continuum client for mobile phones
Copyright (C) 2010 Kingsley Masters
Email: kshade2001 at users.sourceforge.net
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
REVISIONS:
*/
package com.subspace.network;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import android.content.Context;
import android.util.Log;
import android.util.SparseArray;
/**
struct C2SSimplePing {
u32 timestamp;
};
the server replies with 8 bytes:
struct S2CSimplePing {
u32 total;
u32 timestamp;
};
*/
public class NetworkSubspace extends Network implements INetworkCallback {
//these can be set via preferences
public static boolean LOG_CORE_PACKETS = false;
public static boolean LOG_CONNECTION = true;
private static final String TAG = "Subspace";
public static final String CHAR_ENCODING = "ISO-8859-1";
public static final int MAX_RETRY = 5;
public static final int CONNECT_WAIT_TIME = 4000; //4 second
int sentNumber = 0;
int playerCount = -1;
int retryCount = 0;
INetworkEncryption enc;
boolean connected = false;
//reliable packet handling
int reliableNextOutbound;
int reliableNextExpected;
SparseArray<byte[]> reliableIncoming;
HashMap<Integer, byte[]> reliableOutgoing;
Timer reliableResendTimer = new Timer();
//chunked message
byte[] chunkArray = null;
//stream
int streamSize = 0;
int streamCount = 0; // Handles chunk pkt sizes
File streamFileCache;
FileOutputStream streamFileOutputStream;
ISubspaceCallback subspaceCallback;
protected Context _context;
public boolean isConnected() {
return connected;
}
public NetworkSubspace(Context context) {
super(null);
_context = context;
//set call back
setCallback(this);
enc = new NetworkEncryptionVIE();
//create stream backing file
File cacheDir= context.getCacheDir();
streamFileCache = new File(cacheDir, "download.temp");
//delete if it file already exists
if(streamFileCache.exists())
{
streamFileCache.delete();
}
//setup reliable
reliableNextOutbound = 0;
reliableNextExpected = 0;
reliableIncoming = new SparseArray<byte[]>();
reliableOutgoing = new HashMap<Integer,byte[]>();
reliableResendTimer.scheduleAtFixedRate(new TimerTask()
{
public void run() {
if (reliableOutgoing.size() > 0) {
// resend any in queue
Log.d(TAG, "Resending Packets " + reliableOutgoing.size());
for (byte[] bytes : reliableOutgoing.values()) {
ByteBuffer bb = ByteBuffer.wrap(bytes);
bb.order(ByteOrder.LITTLE_ENDIAN);
try {
SSSend(bb);
} catch (IOException e) {
// TODO Auto-generated catch block
Log.e(TAG, Log.getStackTraceString(e));
}
}
}
}
}, 1000, 1000); // resend every second
}
public final void setDownloadCallback(ISubspaceCallback callback) {
this.subspaceCallback = callback;
}
public final boolean SSConnect(String host, int port) throws IOException {
connected = false;
this.Connect(host,port);
//send connection
synchronized (this) {
//block for wait
try {
retryCount = 0;
while ((!connected) && (retryCount < MAX_RETRY)) {
retryCount++;
//send login
this.Send(enc.CreateLogin());
this.wait(CONNECT_WAIT_TIME);
}
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
return connected;
}
public final void SSDisconnect() throws IOException {
//send 1 disconnect
this.SSSend(NetworkPacket.CreateDisconnect());
//signal that we've disconnected
this.connected = false;
Log.d(TAG,"Sent Disconnect");
//send disconnect 2 times to make sure
this.SSSend(NetworkPacket.CreateDisconnect());
this.SSSend(NetworkPacket.CreateDisconnect());
//now forceable close the connection
this.Close();
}
public final void SSSend(ByteBuffer buffer) throws IOException {
if(LOG_CORE_PACKETS)
{
Log.v(TAG,"CS: " + Util.ToHex(buffer));
}
buffer.rewind();
enc.Encrypt(buffer);
this.Send(buffer);
}
public final void SSSendReliable(ByteBuffer bytes) throws IOException {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"Sent Reliable packet " + reliableNextOutbound);
}
ByteBuffer reliablePacket = NetworkPacket.CreateReliable(reliableNextOutbound, bytes);
reliablePacket.rewind();
//save into array
byte[] msg = new byte[reliablePacket.limit()];
//read into msg
reliablePacket.get(msg,0,msg.length);
reliablePacket.rewind();
//save to check if we've received
reliableOutgoing.put(reliableNextOutbound,msg);
//now send
this.SSSend(reliablePacket);
reliableNextOutbound++;
}
public final void SSSync() throws IOException {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"Sent Sync packet");
}
this.SSSend(NetworkPacket.CreateSync(this.getRecvDatagramCount(), this.getSentDatagramCount()));
}
public ByteBuffer Recv(ByteBuffer data, boolean decryptPacket) {
if (data == null) {
return null;
}
try {
//if not connected pass to encryption handler
if (!this.connected && (data.get(0) == NetworkPacket.CORE)) {
//log packets
if(LOG_CORE_PACKETS)
{
Log.v(TAG,Util.ToHex(data));
}
if (data.get(1) == NetworkPacket.CORE_CONNECTIONACK) {
if(LOG_CONNECTION)
{
Log.d(TAG,"Received ConnectionAck: " + Util.ToHex(data));
}
synchronized (this) {
if (enc.HandleLoginAck(data)) {
this.connected = true;
this.notify();
} else {
Log.d(TAG,Util.ToHex(data));
}
}
}
else if (data.get(1) == NetworkPacket.CORE_SYNC) //handle subgame flood protection
{
if(LOG_CONNECTION)
{
Log.d(TAG,"Received Sync: " + Util.ToHex(data));
}
ByteBuffer syncResponse = NetworkPacket.CreateSyncResponse(data.getInt(2));
this.SSSend(syncResponse);
if(LOG_CONNECTION)
{
Log.d(TAG,"Send back Sync: " + Util.ToHex(syncResponse.array()));
}
} else if (data.get(1) == NetworkPacket.CORE_DISCONNECT) {
synchronized (this) {
this.connected = false;
this.notify();
Log.i(TAG,"Disconnected");
}
} else {
Log.i(TAG,"Unrecognised Packet: " + Util.ToHex(data));
}
}
else
{
//if packet needs decrypting
//do so
if (decryptPacket) {
enc.Decrypt(data);
}
//log packets
if(LOG_CORE_PACKETS)
{
if(data.limit() > 520)
{
Log.v(TAG,"CR: packet in excess of 520 recieived, cannot log at the moment");
} else {
Log.v(TAG,"CR: " + Util.ToHex(data));
}
}
byte packetType = data.get(0);
if (packetType == NetworkPacket.CORE) {
byte corePacketType = data.get(1);
switch (corePacketType) {
case NetworkPacket.CORE_RELIABLE: {
int id = data.getInt(2);
//send ack twice
this.SSSend(NetworkPacket.CreateReliableAck(id));
this.SSSend(NetworkPacket.CreateReliableAck(id));
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"New Reliable Packet Received: " + id);
}
//now handle
if (id == this.reliableNextExpected) {
this.reliableNextExpected++;
//its expected so send it for processing
data.position(6);
//copy to new buffer, but remember to update endian
ByteBuffer sliceBuffer = data.slice();
sliceBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.callback.Recv(sliceBuffer, false);
//now check queue
if (this.reliableIncoming.size() > 0) {
while (true) {
//get stored value
byte[] b = (byte[]) this.reliableIncoming.get(this.reliableNextExpected);
//remove it
this.reliableIncoming.remove(this.reliableNextExpected);
//return null if no more left
if (b == null) {
return null;
}
//Increment next expected
reliableNextExpected++;
//copy to new buffer, but remember to update endian
ByteBuffer buffer = ByteBuffer.wrap(b);
buffer.order(ByteOrder.LITTLE_ENDIAN);
//now process received byte
this.callback.Recv(buffer, false);
}
}
} else if (id > this.reliableNextExpected) {
byte[] msg = new byte[data.limit() - 6];
//read into msg
data.position(6);
data.get(msg,0,msg.length);
this.reliableIncoming.put(id, msg);
} else {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"Already received, resending ack for: " + id);
}
//we've already had this packet so send a 2 more acks to make sure we don't get it again
this.SSSend(NetworkPacket.CreateReliableAck(id));
this.SSSend(NetworkPacket.CreateReliableAck(id));
return null;
}
}
case NetworkPacket.CORE_RELIABLEACK: {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_RELIABLEACK " + data.getInt(2));
}
reliableOutgoing.remove(data.getInt(2));
break;
}
case NetworkPacket.CORE_SYNC: {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_SYNC");
}
//send back ack
this.SSSend(NetworkPacket.CreateSyncResponse(data.getInt(2)));
break;
}
case NetworkPacket.CORE_SYNCACK: {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_SYNCACK");
}
break;
}
case NetworkPacket.CORE_DISCONNECT:
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_DISCONNECT");
}
SSDisconnect();
return null;
case NetworkPacket.CORE_CHUNK: {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_CHUNK");
}
int oldSize;
if (chunkArray == null) {
chunkArray = new byte[data.limit() - 2];
data.position(2); data.get(chunkArray, 0, data.limit() - 2);
} else {
oldSize = chunkArray.length;
byte[] newArray = new byte[oldSize + data.limit() - 2];
System.arraycopy(chunkArray, 0, newArray, 0, chunkArray.length);
chunkArray = newArray;
newArray = null;
data.position(2); data.get(chunkArray, oldSize, data.limit() - 2);
}
}
break;
case NetworkPacket.CORE_CHUNKEND: {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_CHUNKEND");
}
int oldSize;
oldSize = chunkArray.length;
byte[] newArray = new byte[oldSize + data.limit() - 2];
System.arraycopy(chunkArray, 0, newArray, 0, chunkArray.length);
chunkArray = newArray;
newArray = null;
data.position(2); data.get(chunkArray, oldSize, data.limit() - 2);
//copy to new buffer, but remember to update endian
ByteBuffer buffer = ByteBuffer.wrap(chunkArray);
buffer.order(ByteOrder.LITTLE_ENDIAN);
this.callback.Recv(buffer, false);
chunkArray = null;
}
break;
case NetworkPacket.CORE_STREAM: {
if (streamCount == 0) {
streamSize = data.getInt(2);
streamCount = data.getInt(2);
streamFileOutputStream = new FileOutputStream (streamFileCache);
if(this.subspaceCallback!=null)
{
this.subspaceCallback.DownloadStarted();
}
}
//write buffer into file
data.position(6);
byte[] buf = new byte[data.limit()-6];
data.get(buf);
streamFileOutputStream.write(buf);
//Decrease stream count
streamCount -= (data.limit() - 6);
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"Stream: " + (streamSize-streamCount) +"/" + streamSize);
}
if(this.subspaceCallback!=null)
{
this.subspaceCallback.DownloadProgressUpdate(streamSize-streamCount, streamSize);
}
if (streamCount <= 0) {
//close stream
streamFileOutputStream.close();
//copy it into memory
byte[] b = new byte[(int)streamFileCache.length()];
FileInputStream fis = new FileInputStream(streamFileCache);
fis.read(b);
fis.close();
ByteBuffer streamArray = ByteBuffer.wrap(b);
streamArray.order(ByteOrder.LITTLE_ENDIAN);
streamArray.rewind();
//send update
if(this.subspaceCallback!=null)
{
this.subspaceCallback.DownloadComplete();
}
//now send on
this.callback.Recv(streamArray, false);
//delete file
streamFileCache.delete();
}
}
break;
case NetworkPacket.CORE_STREAMCANCEL:
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_STREAMCANCEL");
}
break;
case NetworkPacket.CORE_STREAMCANCELACK:
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_STREAMCANCELACK");
}
break;
case NetworkPacket.CORE_CLUSTER: {
if(LOG_CORE_PACKETS)
{
Log.d(TAG,"CORE_CLUSTER");
}
int i = 2;
int size;
byte[] subMessage;
while (i < data.limit()) {
size = data.get(i) & 0xff;
subMessage = new byte[size];
data.position(i+1); data.get(subMessage, 0, size);
ByteBuffer buffer = ByteBuffer.wrap(subMessage);
buffer.order(ByteOrder.LITTLE_ENDIAN);
this.callback.Recv(buffer, false);
i += size + 1;
}
break;
}
default:
{
if(data.limit() > 520)
{
Log.i(TAG,"Unrecognised Core Packet: packet in excess of 520 recieived, cannot log at the moment");
} else {
Log.i(TAG,"Unrecognised Core Packet: " + Util.ToHex(data));
}
}
}
return null;
}
//not a core packet so just return the data
return data;
}
} catch (IOException ioe) {
Log.e(TAG,Log.getStackTraceString(ioe));
}
return null;
}
}