I am relatively new to Android programming: I had a lot of experience with Java and had Android devices with me for long, but for some odd reason I always postponed my first Android app coding experience to this day.
Well, Android is a neat platform, and coding for it really is fun. Aside some things I still find weird in the language (not a big fan of Java) I really am enjoying my time developing for the platform, and the learning curve has been incredibly easy.
I'm developing a VR app that needs to display data received from a server running on a remote computer: in other words, sockets.
After a bit of research, I found some interesting implementations, some of which targeted directly for the Android ecosystem: specifically, some of them use the AsyncTask abstract class, using the progress tracking capabilities to notify listeners of an incoming message.
After a bit of study though, this really seems a strange decision to me: the official documentation clearly states that AsyncTasks should be used for short lived operations, just to keep the UI thread running without any visible block, and that long operations should use other mechanics (such as Executors).
I had in mind something really simple for my Tcp Client (nothing more than a couple of methods like connect(), disconnect() and send() to be called from activities), so I wanted to hide all of the asynchronous stuff from the classes that had to interact with it, putting them all inside the client itself.
Being executors nothing more than wrappers around Threads, I decided to directly create threads, so that this package could be ported easily to the non-android Java world.
The last thing was being notified of the incoming messages: as mentioned before, of course it was impossible for me to use something like onProgressUpdate so I had to stick with the good old Observer Observable pattern, hoping one day Java will come out with a better event support (C# anyone?)
But enough words, let's see some code.
Implementation
After creating a new package in the app, let's declare a couple of enums that will help us track the connection and client status:
public enum TcpClientState {
DISCONNECTED,
CONNECTING,
CONNECTED,
CONNECTION_STARTED,
FAILED
}
Also, we are going to fire events for everything could possibly be interesting for anyone using our client: let's declare another enum that will help listeners detect what's happening.
These are all the events I found useful to have in my implementation, but could be too many or redundant depending on your needs.
public enum TcpEventType {
CONNECTION_STARTED,
CONNECTION_ESTABLISHED,
CONNECTION_FAILED,
CONNECTION_LOST,
MESSAGE_RECEIVED,
MESSAGE_SENT,
DISCONNECTED
}
This will be used by a support class, the TcpEvent, used to encapsulate event types and the actual data:
public class TcpEvent {
private TcpEventType eventType;
private Object payload;
public TcpEvent(TcpEventType eventType, Object payload) {
this.eventType = eventType;
this.payload = payload;
}
public TcpEventType getTcpEventType() {
return this.eventType;
}
public Object getPayload() {
return this.payload;
}
}
In this example i kept the payload generic. It's easy to return whatever you want in a more specific implementation depending on your app domain.
Now, the client itself.
public class TcpClient extends Observable {
private static final String TAG = "TcpClient";
private String address;
private Integer port;
private Integer timeout = 2000;
private TcpClientState state = TcpClientState.DISCONNECTED;
PrintWriter bufferOut;
BufferedReader bufferIn;
private Socket socket;
public TcpClient() { }
public TcpClient(String address, int port) {
this.address = address;
this.port = port;
}
protected void fireEvent(TcpEvent event) {
setChanged();
notifyObservers(event);
clearChanged();
}
}
Note the fireEvent protected helper, that will help us write a bit less of code notifying observers.
Also, don't forget to add the right getters and setters for the port and address properties: having the socket state explicitly declared can help us prevent strange behaviors and errors, like this:
public void setPort(int port) {
if(state == TcpClientState.CONNECTED) {
throw new RuntimeException("Cannot change port while connected");
}
this.port = port;
}
Now, let's write the actual code needed to perform the connection: using inner classes will give us access to the parent's variables while inheriting all the Thread good stuff.
Inside the client class we will have this method:
public void connect() {
if(state == TcpClientState.DISCONNECTED || state == TcpClientState.FAILED) {
if(address == null || port == null) {
throw new RuntimeException("Address or port missing");
}
new ConnectThread().start();
} else {
throw new RuntimeException("This client is already connected or connecting");
}
}
And the related Thread:
private class ConnectThread extends Thread {
@Override
public void run() {
try {
state = TcpClientState.CONNECTING;
fireEvent(new TcpEvent(TcpEventType.CONNECTION_STARTED, null));
socket = new Socket();
socket.connect(new InetSocketAddress(InetAddress.getByName(address), port), timeout);
bufferOut = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
bufferIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
state = TcpClientState.CONNECTED;
fireEvent(new TcpEvent(TcpEventType.CONNECTION_ESTABLISHED, null));
new ReceiveMessagesThread().start();
} catch(SocketTimeoutException e) {
fireEvent(new TcpEvent(TcpEventType.CONNECTION_FAILED, e));
Log.e(TAG, "Socket timed out: " + e);
state = TcpClientState.FAILED;
} catch(IOException e) {
fireEvent(new TcpEvent(TcpEventType.CONNECTION_FAILED, e));
Log.e(TAG, "Could not connect to host: " + e);
state = TcpClientState.FAILED;
}
}
}
And of course this will bring us to the ReceiveMessagesThread:
private class ReceiveMessagesThread extends Thread {
@Override
public void run() {
while(state == TcpClientState.CONNECTED) {
try {
String message = bufferIn.readLine();
if(message != null) {
fireEvent(new TcpEvent(TcpEventType.MESSAGE_RECEIVED, message));
}
} catch(IOException e) {
fireEvent(new TcpEvent(TcpEventType.CONNECTION_LOST, null));
try {
bufferOut.flush();
bufferOut.close();
bufferIn.close();
socket.close();
} catch (IOException er) {
Log.e(TAG, "Error clearing connection: " + er);
}
state = TcpClientState.DISCONNECTED;
}
}
}
}
To send a message to the server, we are going to use this method:
public void sendMessage(String message) {
if(state == TcpClientState.CONNECTED) {
new SendMessageThread(message).start();
} else {
throw new RuntimeException("This client is not connected, and cannot send any message");
}
}
With the related thread:
private class SendMessageThread extends Thread {
private String messageLine;
public SendMessageThread(String message) {
this.messageLine = message + "\n";
}
@Override
public void run() {
if(bufferOut.checkError()) {
try {
bufferOut.flush();
bufferOut.close();
bufferIn.close();
} catch(IOException e) {
Log.e(TAG, "Error sending this message: " + e);
}
} else {
bufferOut.print(messageLine);
bufferOut.flush();
fireEvent(new TcpEvent(TcpEventType.MESSAGE_SENT, messageLine.toString()));
}
}
}
Notice that I'm appending a newline char to the string: this is done to split messages, and any other divider can be used. It's just something I'm used to do, and many tcp classes in many languages have methods to read and write lines out of the box.
The last thing we need is a way to end the communication:
public void disconnect() {
new DisconnectThread().run();
}
private class DisconnectThread extends Thread {
@Override
public void run() {
try {
bufferOut.flush();
bufferOut.close();
bufferIn.close();
socket.close();
} catch(IOException e) {
Log.e(TAG, "Error disconnecting this client: " + e);
}
fireEvent(new TcpEvent(TcpEventType.DISCONNECTED, null));
}
}
Now, in your android project AndroidManifest.xml
file specify that you need network access:
<uses-permission android:name="android.permission.INTERNET" />
And in your activities the only thing you need to do will be:
public class MainActivity extends AppCompatActivity implements Observer {
TcpClient client;
@Override
protected void onCreate(Bundle savedInstanceState) {
this.client = new TcpClient("localhost", 8080);
this.client.addObserver(this);
this.connect();
}
@Override
public void update(Observable o, Object arg) {
TcpEvent event = (TcpEvent)arg;
switch (event.getTcpEventType()) {
case MESSAGE_RECEIVED:
//Do something
break;
case CONNECTION_ESTABLISHED:
runOnUiThread(new Runnable() {
public void run() {
//Update ui
}
});
break;
}
Just remember to call runOnUiThread if you need to update your activity ui: after all messages are going to be sent from a different thread than the ui one, required to manage view updates in Android.
Hope this is useful!