SlideShare a Scribd company logo
Creating a WhatsApp Clone - Part II
We’ll jump into the client functionality from the server connectivity class. I won’t start with the UI and build everything up but instead go through the code relatively
quickly as I’m assuming you’ve gone through the longer explanations in the previous modules.
* need additional information or have any questions.
*/
package com.codename1.whatsapp.model;
import com.codename1.contacts.Contact;
import com.codename1.io.JSONParser;
import com.codename1.io.Log;
import com.codename1.io.Preferences;
import com.codename1.io.Util;
import com.codename1.io.rest.RequestBuilder;
import com.codename1.io.rest.Response;
import com.codename1.io.rest.Rest;
import com.codename1.io.websocket.WebSocket;
import com.codename1.properties.PropertyIndex;
import static com.codename1.ui.CN.*;
import com.codename1.ui.Display;
import com.codename1.ui.EncodedImage;
import com.codename1.util.EasyThread;
import com.codename1.util.OnComplete;
import com.codename1.util.regex.StringReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
Server
Like before the Server class abstracts the backend. I’ll soon go into the details of the other classes in this package which are property business object abstractions.

As a reminder notice that I import the CN class so I can use shorthand syntax for various API’s. I do this in almost all files in the project.
import com.codename1.ui.Display;
import com.codename1.ui.EncodedImage;
import com.codename1.util.EasyThread;
import com.codename1.util.OnComplete;
import com.codename1.util.regex.StringReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
Server
Right now the debug environment points at the local host but in order to work with devices this will need to point at an actual URL or IP address
import com.codename1.ui.Display;
import com.codename1.ui.EncodedImage;
import com.codename1.util.EasyThread;
import com.codename1.util.OnComplete;
import com.codename1.util.regex.StringReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
Server
As I mentioned before we’ll store the data as JSON in storage. The file names don’t have to end in “.json”, I just did that for our convenience.
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
private static boolean connected;
private static List<ChatMessage> messageQueue;
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
Server
This is a property business object we’ll discuss soon. We use it to represent all our contacts and outselves
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
private static boolean connected;
private static List<ChatMessage> messageQueue;
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
Server
This is the current websocket connection, we need this to be global as we will disconnect from the server when the app is minimized. That’s important otherwise battery
saving code might kill the app
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
private static boolean connected;
private static List<ChatMessage> messageQueue;
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
Server
This flag indicates whether he websocket is connected which saves us from asking the connection if it’s still active.
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
private static boolean connected;
private static List<ChatMessage> messageQueue;
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
Server
If we aren't connected new messages go into the message queue and will go out when we reconnect.
import java.util.List;
import java.util.Map;
public class Server {
private static final String SERVER_URL = "http://localhost:8080/";
private static final String WEBSOCKER_URL = "ws://localhost:8080/socket";
private static final String USER_FILE_NAME = "user.json";
private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json";
private static ChatContact currentUser;
private static WebSocket connection;
private static boolean connected;
private static List<ChatMessage> messageQueue;
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
Server
The user logged into the app is global
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) {
messageQueue = new ChatMessage().getPropertyIndex().
loadJSONList(MESSAGE_QUEUE_FILE_NAME);
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().getPropertyIndex().
loadJSONList("contacts.json");
} else {
contactCache = new ArrayList<>();
}
} else {
contactCache = new ArrayList<>();
}
}
Server
The init method is invoked when the app is loaded, it loads the global data from storage and sets the variable values. Normally there should be data here with the special
case of the first activation.
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) {
messageQueue = new ChatMessage().getPropertyIndex().
loadJSONList(MESSAGE_QUEUE_FILE_NAME);
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().getPropertyIndex().
loadJSONList("contacts.json");
} else {
contactCache = new ArrayList<>();
}
} else {
contactCache = new ArrayList<>();
}
}
Server
If this is the first activation before receiving the validation SMS this file won’t exist. In that case we’ll just initialize the contact cache as an empty list and be on our way.
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) {
messageQueue = new ChatMessage().getPropertyIndex().
loadJSONList(MESSAGE_QUEUE_FILE_NAME);
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().getPropertyIndex().
loadJSONList("contacts.json");
} else {
contactCache = new ArrayList<>();
}
} else {
contactCache = new ArrayList<>();
}
}
Server
Assuming we are logged in we can load the data for the current user this is pretty easy to do for property business objects.
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) {
messageQueue = new ChatMessage().getPropertyIndex().
loadJSONList(MESSAGE_QUEUE_FILE_NAME);
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().getPropertyIndex().
loadJSONList("contacts.json");
} else {
contactCache = new ArrayList<>();
}
} else {
contactCache = new ArrayList<>();
}
}
Server
If there are messages in the message queue we need to load them as well. This can happen if the user sends a message without connectivity and the app is killed
public static ChatContact user() {
return currentUser;
}
public static void init() {
if(existsInStorage(USER_FILE_NAME)) {
currentUser = new ChatContact();
currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME);
if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) {
messageQueue = new ChatMessage().getPropertyIndex().
loadJSONList(MESSAGE_QUEUE_FILE_NAME);
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().getPropertyIndex().
loadJSONList("contacts.json");
} else {
contactCache = new ArrayList<>();
}
} else {
contactCache = new ArrayList<>();
}
}
Server
Contacts are cached here, the contacts essentially contain everything in the app. This might be a bit wasteful to store all the data in this way but it should work
reasonably even for relatively large datasets
} else {
contactCache = new ArrayList<>();
}
}
public static void flushMessageQueue() {
if(connected && messageQueue != null && messageQueue.size() > 0) {
for(ChatMessage m : messageQueue) {
connection.send(m.getPropertyIndex().toJSON());
}
messageQueue.clear();
}
}
private static RequestBuilder post(String u) {
RequestBuilder r = Rest.post(SERVER_URL + u).jsonContent();
if(currentUser != null && currentUser.token.get() != null) {
r.header("auth", currentUser.token.get());
}
return r;
}
private static RequestBuilder get(String u) {
RequestBuilder r = Rest.get(SERVER_URL + u).jsonContent();
Server
This method sends the content of the message queue, it’s invoked when we go back online
}
messageQueue.clear();
}
}
private static RequestBuilder post(String u) {
RequestBuilder r = Rest.post(SERVER_URL + u).jsonContent();
if(currentUser != null && currentUser.token.get() != null) {
r.header("auth", currentUser.token.get());
}
return r;
}
private static RequestBuilder get(String u) {
RequestBuilder r = Rest.get(SERVER_URL + u).jsonContent();
if(currentUser != null && currentUser.token.get() != null) {
r.header("auth", currentUser.token.get());
}
return r;
}
public static void login(OnComplete<ChatContact> c) {
post("user/login").
body(currentUser).fetchAsProperties(
Server
These methods are shorthand for get and post methods of the Rest API. They force JSON usage and add the auth header which most of the server side API’s will need.
That lets us write shorter code.
r.header("auth", currentUser.token.get());
}
return r;
}
public static void login(OnComplete<ChatContact> c) {
post("user/login").
body(currentUser).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
currentUser.
getPropertyIndex().
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static void signup(ChatContact user,
OnComplete<ChatContact> c) {
post("user/signup").
body(user).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
Server
The login method is the first server side method. It doesn’t do much, it sends the current user to the server then saves the returned instance of that user. This allows us to
refresh user data from the server.
r.header("auth", currentUser.token.get());
}
return r;
}
public static void login(OnComplete<ChatContact> c) {
post("user/login").
body(currentUser).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
currentUser.
getPropertyIndex().
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static void signup(ChatContact user,
OnComplete<ChatContact> c) {
post("user/signup").
body(user).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
Server
We pass the current user as the body in an argument, notice I can pass the property business object directly and it will be converted to JSON.
r.header("auth", currentUser.token.get());
}
return r;
}
public static void login(OnComplete<ChatContact> c) {
post("user/login").
body(currentUser).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
currentUser.
getPropertyIndex().
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static void signup(ChatContact user,
OnComplete<ChatContact> c) {
post("user/signup").
body(user).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
Server
In the response we read the user replace the current instance and save it to disk.
c.completed(currentUser);
},
ChatContact.class);
}
public static void signup(ChatContact user,
OnComplete<ChatContact> c) {
post("user/signup").
body(user).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
currentUser.
getPropertyIndex().
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static void update(OnComplete<ChatContact> c) {
post("user/update").
body(currentUser).fetchAsProperties(
res -> {
Server
Signup is very similar to login, in fact it’s identical. However, after signup is complete you still don’t have anything since we need to verify the user, so lets skip down to
that
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static boolean verify(String code) {
Response<String> result = get("user/verify").
queryParam("userId", currentUser.id.get()).
queryParam("code", code).
getAsString();
return "OK".equals(result.getResponseData());
}
public static void sendMessage(ChatMessage m, ChatContact cont) {
cont.lastActivityTime.set(new Date());
cont.chats.add(m);
saveContacts();
if(connected) {
post("user/sendMessage").
body(m).
fetchAsProperties(e -> {
cont.chats.remove(m);
cont.chats.add((ChatMessage)e.getResponseData());
Server
On the server, signup triggers an SMS which we need to intercept. We then need to send the SMS code via this API. Only after this method returns OK our user becomes
valid.
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static void update(OnComplete<ChatContact> c) {
post("user/update").
body(currentUser).fetchAsProperties(
res -> {
currentUser = (ChatContact)res.getResponseData();
currentUser.
getPropertyIndex().
storeJSON(USER_FILE_NAME);
c.completed(currentUser);
},
ChatContact.class);
}
public static boolean verify(String code) {
Response<String> result = get("user/verify").
queryParam("userId", currentUser.id.get()).
queryParam("code", code).
getAsString();
Server
Update is practically identical to the two other methods but sends the updated data from the client to the server. It isn’t interesting.
getAsString();
return "OK".equals(result.getResponseData());
}
public static void sendMessage(ChatMessage m, ChatContact cont) {
cont.lastActivityTime.set(new Date());
cont.chats.add(m);
saveContacts();
if(connected) {
post("user/sendMessage").
body(m).
fetchAsProperties(e -> {
cont.chats.remove(m);
cont.chats.add((ChatMessage)e.getResponseData());
saveContacts();
}, ChatMessage.class);
//connection.send(m.getPropertyIndex().toJSON());
} else {
if(messageQueue == null) {
messageQueue = new ArrayList<>();
}
messageQueue.add(m);
PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME,
messageQueue);
Server
send message is probably the most important method here. It delivers a message to the server and saves it into the JSON storage.
getAsString();
return "OK".equals(result.getResponseData());
}
public static void sendMessage(ChatMessage m, ChatContact cont) {
cont.lastActivityTime.set(new Date());
cont.chats.add(m);
saveContacts();
if(connected) {
post("user/sendMessage").
body(m).
fetchAsProperties(e -> {
cont.chats.remove(m);
cont.chats.add((ChatMessage)e.getResponseData());
saveContacts();
}, ChatMessage.class);
//connection.send(m.getPropertyIndex().toJSON());
} else {
if(messageQueue == null) {
messageQueue = new ArrayList<>();
}
messageQueue.add(m);
PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME,
messageQueue);
Server
Here we save the time in which a specific contact last chatted, this allows us to sort the contacts based on the time a specific contact last chatted with us
getAsString();
return "OK".equals(result.getResponseData());
}
public static void sendMessage(ChatMessage m, ChatContact cont) {
cont.lastActivityTime.set(new Date());
cont.chats.add(m);
saveContacts();
if(connected) {
post("user/sendMessage").
body(m).
fetchAsProperties(e -> {
cont.chats.remove(m);
cont.chats.add((ChatMessage)e.getResponseData());
saveContacts();
}, ChatMessage.class);
//connection.send(m.getPropertyIndex().toJSON());
} else {
if(messageQueue == null) {
messageQueue = new ArrayList<>();
}
messageQueue.add(m);
PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME,
messageQueue);
Server
This sends the message using a webservice. The message body is submitted as a ChatMessage business object which is implicitly translated to JSON
getAsString();
return "OK".equals(result.getResponseData());
}
public static void sendMessage(ChatMessage m, ChatContact cont) {
cont.lastActivityTime.set(new Date());
cont.chats.add(m);
saveContacts();
if(connected) {
post("user/sendMessage").
body(m).
fetchAsProperties(e -> {
cont.chats.remove(m);
cont.chats.add((ChatMessage)e.getResponseData());
saveContacts();
}, ChatMessage.class);
//connection.send(m.getPropertyIndex().toJSON());
} else {
if(messageQueue == null) {
messageQueue = new ArrayList<>();
}
messageQueue.add(m);
PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME,
messageQueue);
Server
Initially I sent messages via the websocket but there wasn’t a big benefit to doing that. I kept that code in place for reference. The advantage of using a websocket is
mostly in the server side where calls are seamlessly translated.
public static void sendMessage(ChatMessage m, ChatContact cont) {
cont.lastActivityTime.set(new Date());
cont.chats.add(m);
saveContacts();
if(connected) {
post("user/sendMessage").
body(m).
fetchAsProperties(e -> {
cont.chats.remove(m);
cont.chats.add((ChatMessage)e.getResponseData());
saveContacts();
}, ChatMessage.class);
//connection.send(m.getPropertyIndex().toJSON());
} else {
if(messageQueue == null) {
messageQueue = new ArrayList<>();
}
messageQueue.add(m);
PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME,
messageQueue);
}
}
Server
If we are offline the message is added to the message queue and the content of the queue is saved
messageQueue);
}
}
public static void bindMessageListener(final ServerMessages
callback) {
connection = new WebSocket(WEBSOCKER_URL) {
@Override
protected void onOpen() {
connected = true;
long lastMessageTime =
Preferences.get("LastReceivedMessage", (long)0);
send("{"t":"init","tok":"" +
currentUser.token.get() +
"","time":" +
lastMessageTime + "}");
callSerially(() -> callback.connected());
final WebSocket w = this;
new Thread() {
public void run() {
Util.sleep(80000);
while(connection == w) {
// keep-alive message every 80 seconds to avoid
// cloudflare killing of the connection
Server
This method binds the websocket to the server and handles incoming/outgoing messages over the websocket connection. This is a pretty big method because of the
inner class within it, but it’s relatively simple as the inner class is mostly trivial.

The bind method receives a callback interface for various application level events. E.g. when a message is received we’d like to update the UI to indicate that. We can do
that via the callback interface without getting all of that logic into the server class.
messageQueue);
}
}
public static void bindMessageListener(final ServerMessages
callback) {
connection = new WebSocket(WEBSOCKER_URL) {
@Override
protected void onOpen() {
connected = true;
long lastMessageTime =
Preferences.get("LastReceivedMessage", (long)0);
send("{"t":"init","tok":"" +
currentUser.token.get() +
"","time":" +
lastMessageTime + "}");
callSerially(() -> callback.connected());
final WebSocket w = this;
new Thread() {
public void run() {
Util.sleep(80000);
while(connection == w) {
// keep-alive message every 80 seconds to avoid
// cloudflare killing of the connection
Server
Here we create a subclass of websocket and override all the relevant callback methods.
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Log.e(err);
throw new RuntimeException(err.toString());
}
}
@Override
protected void onMessage(byte[] message) {
}
@Override
protected void onError(Exception ex) {
Log.e(ex);
}
};
connection.autoReconnect(5000);
connection.connect();
}
private static void updateMessage(ChatMessage m) {
for(ChatContact c : contactCache) {
Server
Skipping to the end of the method we can see the connection call and also the autoReconnect method which automatically tries to reconnect every 5 seconds if we lost
the websocket connection.
public static void bindMessageListener(final ServerMessages
callback) {
connection = new WebSocket(WEBSOCKER_URL) {
@Override
protected void onOpen() {
connected = true;
long lastMessageTime =
Preferences.get("LastReceivedMessage", (long)0);
send("{"t":"init","tok":"" +
currentUser.token.get() +
"","time":" +
lastMessageTime + "}");
callSerially(() -> callback.connected());
final WebSocket w = this;
new Thread() {
public void run() {
Util.sleep(80000);
while(connection == w) {
// keep-alive message every 80 seconds to avoid
// cloudflare killing of the connection
// https://community.cloudflare.com/t/cloudflare-
websocket-timeout/5865/3
send("{"t":"ping"}");
Util.sleep(80000);
Server
Let’s go back to the callback methods starting with onOpen(). This method is invoked when the connection is established. Once this is established we can start making
websocket calls and receiving messages.
public static void bindMessageListener(final ServerMessages
callback) {
connection = new WebSocket(WEBSOCKER_URL) {
@Override
protected void onOpen() {
connected = true;
long lastMessageTime =
Preferences.get("LastReceivedMessage", (long)0);
send("{"t":"init","tok":"" +
currentUser.token.get() +
"","time":" +
lastMessageTime + "}");
callSerially(() -> callback.connected());
final WebSocket w = this;
new Thread() {
public void run() {
Util.sleep(80000);
while(connection == w) {
// keep-alive message every 80 seconds to avoid
// cloudflare killing of the connection
// https://community.cloudflare.com/t/cloudflare-
websocket-timeout/5865/3
send("{"t":"ping"}");
Util.sleep(80000);
Server
We start by sending an init message, This is a simple JSON message that provides the authorization token for the current user and the time of the last message received.
This means the server now knows we are connected and knows the time of the message we last received, it means that if the server has messages pending it can send
them now.
public static void bindMessageListener(final ServerMessages
callback) {
connection = new WebSocket(WEBSOCKER_URL) {
@Override
protected void onOpen() {
connected = true;
long lastMessageTime =
Preferences.get("LastReceivedMessage", (long)0);
send("{"t":"init","tok":"" +
currentUser.token.get() +
"","time":" +
lastMessageTime + "}");
callSerially(() -> callback.connected());
final WebSocket w = this;
new Thread() {
public void run() {
Util.sleep(80000);
while(connection == w) {
// keep-alive message every 80 seconds to avoid
// cloudflare killing of the connection
// https://community.cloudflare.com/t/cloudflare-
websocket-timeout/5865/3
send("{"t":"ping"}");
Util.sleep(80000);
Server
Next we send an event that we are connected, notice I used callSerially to send it on the EDT. Since these events will most likely handle GUI this makes sense.
long lastMessageTime =
Preferences.get("LastReceivedMessage", (long)0);
send("{"t":"init","tok":"" +
currentUser.token.get() +
"","time":" +
lastMessageTime + "}");
callSerially(() -> callback.connected());
final WebSocket w = this;
new Thread() {
public void run() {
Util.sleep(80000);
while(connection == w) {
// keep-alive message every 80 seconds to avoid
// cloudflare killing of the connection
// https://community.cloudflare.com/t/cloudflare-
websocket-timeout/5865/3
send("{"t":"ping"}");
Util.sleep(80000);
}
}
}.start();
}
@Override
Server
Finally, we open a thread to send a ping message every 80 seconds. This is redundant for most users and you can remove that code if you don’t use cloudflare. However,
if you do then cloudflare closes connections after 100 seconds of inactivity. That way the connection isn't closed as cloudflare sees that it’s active. 

Cloudflare is a content delivery network we use for our web properties. It helps scale and protect your domain but it isn't essential for this specific deployment. Still I
chose to keep that code in because this took us a while to discover and might be a stumbling block for you as well.
}
}.start();
}
@Override
protected void onClose(int statusCode, String reason) {
connected = false;
callSerially(() -> callback.disconnected());
}
@Override
protected void onMessage(String message) {
try {
StringReader r = new StringReader(message);
JSONParser jp = new JSONParser();
JSONParser.setUseBoolean(true);
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
Server
When a connection is closed we call the event (again on the EDT) and mark the connected flag appropriately.
connected = false;
callSerially(() -> callback.disconnected());
}
@Override
protected void onMessage(String message) {
try {
StringReader r = new StringReader(message);
JSONParser jp = new JSONParser();
JSONParser.setUseBoolean(true);
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
Server
All the messages in the app are text based messages so we use this version of the message callback event to handle incoming messages.
connected = false;
callSerially(() -> callback.disconnected());
}
@Override
protected void onMessage(String message) {
try {
StringReader r = new StringReader(message);
JSONParser jp = new JSONParser();
JSONParser.setUseBoolean(true);
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
Server
Technically the messages are JSON strings, so we convert the String to a reader object. Then we parse the message and pass the result into the property business
object. This can actually be written in a slightly more concise way with the fromJSON() method. However, that method didn't exist when I wrote this code.
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
return;
}
Preferences.set("LastReceivedMessage",
c.time.get().getTime());
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Server
Now that we parsed the object we need to decide what to do with it. We do that on the EDT since the results would process to impact the UI
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
return;
}
Preferences.set("LastReceivedMessage",
c.time.get().getTime());
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Server
The typing flag allows us to send an event that a user is typing, I didn't fully implement this feature but the callback and event behavior is correct.
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
return;
}
Preferences.set("LastReceivedMessage",
c.time.get().getTime());
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Server
Another feature that I didn’t completely finish is the viewed by feature. Here we can process an event indicating there was a change in the list of people who saw a
specific message
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
return;
}
Preferences.set("LastReceivedMessage",
c.time.get().getTime());
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Server
If it’s not one of those then it’s an actual message. We need to start by updating the last received message time. I’ll discuss update message soon, it effectively stores the
message.
JSONParser.setUseLongs(true);
Map m = jp.parseJSON(r);
ChatMessage c = new ChatMessage();
c.getPropertyIndex().
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
return;
}
Preferences.set("LastReceivedMessage",
c.time.get().getTime());
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Server
ackMessage acknowledges to the server that the message was received. This is important otherwise a message might be resent to make sure we received it.
populateFromMap(m, ChatMessage.class);
callSerially(() -> {
if(c.typing.get() != null &&
c.typing.getBoolean()) {
callback.userTyping(c.authorId.get());
return;
}
if(c.viewedBy.size() > 0) {
callback.messageViewed(message,
c.viewedBy.asList());
return;
}
Preferences.set("LastReceivedMessage",
c.time.get().getTime());
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Log.e(err);
throw new RuntimeException(err.toString());
}
}
Server
Finally we invoke the message received callback. Since we are already within a call serially we don’t need to wrap this too.
updateMessage(c);
ackMessage(c.id.get());
callback.messageReceived(c);
});
} catch(IOException err) {
Log.e(err);
throw new RuntimeException(err.toString());
}
}
@Override
protected void onMessage(byte[] message) {
}
@Override
protected void onError(Exception ex) {
Log.e(ex);
}
};
connection.autoReconnect(5000);
connection.connect();
}
Server
We don't use binary messages and most errors would be resolved by autoReconnect. Still it’s important to at least log the errors.
}
};
connection.autoReconnect(5000);
connection.connect();
}
private static void updateMessage(ChatMessage m) {
for(ChatContact c : contactCache) {
if(c.id.get() != null &&
c.id.get().equals(m.authorId.get())) {
c.lastActivityTime.set(new Date());
c.chats.add(m);
saveContacts();
return;
}
}
findRegisteredUserById(m.authorId.get(), cc -> {
contactCache.add(cc);
cc.chats.add(m);
saveContacts();
});
}
public static void closeWebsocketConnection() {
Server
The update method is invoked to update a message in the chat.
}
};
connection.autoReconnect(5000);
connection.connect();
}
private static void updateMessage(ChatMessage m) {
for(ChatContact c : contactCache) {
if(c.id.get() != null &&
c.id.get().equals(m.authorId.get())) {
c.lastActivityTime.set(new Date());
c.chats.add(m);
saveContacts();
return;
}
}
findRegisteredUserById(m.authorId.get(), cc -> {
contactCache.add(cc);
cc.chats.add(m);
saveContacts();
});
}
public static void closeWebsocketConnection() {
Server
First we loop over the existing contacts and try to find the right one. Once we find that contact we can add the message to the contact
}
};
connection.autoReconnect(5000);
connection.connect();
}
private static void updateMessage(ChatMessage m) {
for(ChatContact c : contactCache) {
if(c.id.get() != null &&
c.id.get().equals(m.authorId.get())) {
c.lastActivityTime.set(new Date());
c.chats.add(m);
saveContacts();
return;
}
}
findRegisteredUserById(m.authorId.get(), cc -> {
contactCache.add(cc);
cc.chats.add(m);
saveContacts();
});
}
public static void closeWebsocketConnection() {
Server
The find method finds that contact and we add a new message into the database
contactCache.add(cc);
cc.chats.add(m);
saveContacts();
});
}
public static void closeWebsocketConnection() {
if(connection != null) {
connection.close();
connection = null;
}
}
public static void saveContacts() {
if(contactCache != null && contactsThread != null) {
contactsThread.run(() -> {
PropertyIndex.storeJSONList("contacts.json",
contactCache);
});
}
}
private static List<ChatContact> contactCache;
private static EasyThread contactsThread;
Server
This method closes the websocket connection. It’s something we need to do when the app is suspended so the OS doesn’t kill the app. We’ll discuss this when talking
about the lifecycle methods later
});
}
public static void closeWebsocketConnection() {
if(connection != null) {
connection.close();
connection = null;
}
}
public static void saveContacts() {
if(contactCache != null && contactsThread != null) {
contactsThread.run(() -> {
PropertyIndex.storeJSONList("contacts.json",
contactCache);
});
}
}
private static List<ChatContact> contactCache;
private static EasyThread contactsThread;
public static void fetchContacts(
OnComplete<List<ChatContact>> contactsCallback) {
if(contactsThread == null) {
Server
The contacts are saved on the contacts thread, we use this helper method to go into the helper thread to prevent race conditions
});
}
}
private static List<ChatContact> contactCache;
private static EasyThread contactsThread;
public static void fetchContacts(
OnComplete<List<ChatContact>> contactsCallback) {
if(contactsThread == null) {
contactsThread = EasyThread.start("Contacts Thread");
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(!contactsThread.isThisIt()) {
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(contactCache != null) {
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
Server
Fetch contacts loads the contacts from the JSON list or the device contacts. Since this can be an expensive operation we do it on a separate contacts thread which is an
easy thread.
});
}
}
private static List<ChatContact> contactCache;
private static EasyThread contactsThread;
public static void fetchContacts(
OnComplete<List<ChatContact>> contactsCallback) {
if(contactsThread == null) {
contactsThread = EasyThread.start("Contacts Thread");
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(!contactsThread.isThisIt()) {
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(contactCache != null) {
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
Server
Easy threads let us send tasks to the thread, similarly to callSerially on the EDT. Here we lazily create the easy thread and then run fetchContacts on that thread assuming
the current easy thread is null.
});
}
}
private static List<ChatContact> contactCache;
private static EasyThread contactsThread;
public static void fetchContacts(
OnComplete<List<ChatContact>> contactsCallback) {
if(contactsThread == null) {
contactsThread = EasyThread.start("Contacts Thread");
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(!contactsThread.isThisIt()) {
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(contactCache != null) {
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
Server
If the thread already exists we check whether we already are on the easy thread. Assuming we aren’t on the easy thread we call this method again on the thread and
return. All the following lines are now guaranteed to run on one thread which is the easy thread. As such they are effectively thread safe and won’t slow down the EDT
unless we do something that’s very CPU intensive.
});
}
}
private static List<ChatContact> contactCache;
private static EasyThread contactsThread;
public static void fetchContacts(
OnComplete<List<ChatContact>> contactsCallback) {
if(contactsThread == null) {
contactsThread = EasyThread.start("Contacts Thread");
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(!contactsThread.isThisIt()) {
contactsThread.run(() -> fetchContacts(contactsCallback));
return;
}
if(contactCache != null) {
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
Server
If we already have the data we use callSeriallyOnIdle. This is a slow version of callSerially that waits for the EDT to reach idle state. This is important for performance. A
regular callSerially might occur when the system is animating or in need of resources. If we want to do something expensive or slow it might cause chocking of the UI.
callSeriallyOnIdle will delay the callSerially to a point where there are no pending animations or user interaction, this means that there is enough CPU to perform the
operation.
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
getPropertyIndex().
loadJSONList("contacts.json");
callSerially(() -> contactsCallback.completed(contactCache));
for(ChatContact c : contactCache) {
if(existsInStorage(c.name.get() + ".jpg")) {
String f = c.name.get() + ".jpg";
try (InputStream is = createStorageInputStream(f)) {
c.photo.set(EncodedImage.create(is));
} catch(IOException err) {
Log.e(err);
}
}
}
return;
}
Contact[] contacts = Display.getInstance().getAllContacts(true,
Server
If we have a JSON file for the contacts we use that as a starting point. This allows us to store all the data in one place and mutate the data as we see fit. We keep the
contacts in a contacts cache map which enables fast access at the tradeoff of some RAM. This isn’t too much since we store the thumbnails as external jpegs.
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
getPropertyIndex().
loadJSONList("contacts.json");
callSerially(() -> contactsCallback.completed(contactCache));
for(ChatContact c : contactCache) {
if(existsInStorage(c.name.get() + ".jpg")) {
String f = c.name.get() + ".jpg";
try (InputStream is = createStorageInputStream(f)) {
c.photo.set(EncodedImage.create(is));
} catch(IOException err) {
Log.e(err);
}
}
}
return;
}
Contact[] contacts = Display.getInstance().getAllContacts(true,
Server
Once we loaded the core JSON data we use callSerially to send the event of loading completion, but we aren’t done yet
callSeriallyOnIdle(() ->
contactsCallback.completed(contactCache));
return;
}
if(existsInStorage("contacts.json")) {
contactCache = new ChatContact().
getPropertyIndex().
loadJSONList("contacts.json");
callSerially(() -> contactsCallback.completed(contactCache));
for(ChatContact c : contactCache) {
if(existsInStorage(c.name.get() + ".jpg")) {
String f = c.name.get() + ".jpg";
try (InputStream is = createStorageInputStream(f)) {
c.photo.set(EncodedImage.create(is));
} catch(IOException err) {
Log.e(err);
}
}
}
return;
}
Contact[] contacts = Display.getInstance().getAllContacts(true,
Server
We loop over the contacts we loaded and check if there is an image file matching the contact name. Assuming there is we load it on the contacts thread and set it to the
contact. This will fire an event on the property object and trigger a repaint asynchronously.
}
}
}
return;
}
Contact[] contacts = Display.getInstance().getAllContacts(true,
true, false, true, false, false);
ArrayList<ChatContact> l = new ArrayList<>();
for(Contact c : contacts) {
ChatContact cc = new ChatContact().
phone.set(c.getPrimaryPhoneNumber()).
name.set(c.getDisplayName());
l.add(cc);
callSeriallyOnIdle(() -> {
cc.photo.set(c.getPhoto());
if(cc.photo.get() != null) {
contactsThread.run(() -> {
String f = cc.name.get() + ".jpg";
try(OutputStream os =
createStorageOutputStream(f)) {
EncodedImage img = EncodedImage.
createFromImage(cc.photo.get(), true);
os.write(img.getImageData());
Server
If we don’t have a JSON file we need to create it and the place to start is the contacts on the device. getAllContacts fetches all the device contacts. The first argument is
true if we only want contacts that have phone numbers associated with them. This is true as we don’t need contacts without phone numbers. The next few values
indicate the attributes we need from the contacts database, we don’t need most of the attributes. We only fetch the full name and phone number. The reason for this is
performance, fetching all attributes can be very expensive even on a fast device.
return;
}
Contact[] contacts = Display.getInstance().getAllContacts(true,
true, false, true, false, false);
ArrayList<ChatContact> l = new ArrayList<>();
for(Contact c : contacts) {
ChatContact cc = new ChatContact().
phone.set(c.getPrimaryPhoneNumber()).
name.set(c.getDisplayName());
l.add(cc);
callSeriallyOnIdle(() -> {
cc.photo.set(c.getPhoto());
if(cc.photo.get() != null) {
contactsThread.run(() -> {
String f = cc.name.get() + ".jpg";
try(OutputStream os =
createStorageOutputStream(f)) {
EncodedImage img = EncodedImage.
createFromImage(cc.photo.get(), true);
os.write(img.getImageData());
} catch(IOException err) {
Log.e(err);
}
Server
Next we loop over each contact and add it to the list of contacts. We convert the builtin Contact object to ChatContact in the process.
true, false, true, false, false);
ArrayList<ChatContact> l = new ArrayList<>();
for(Contact c : contacts) {
ChatContact cc = new ChatContact().
phone.set(c.getPrimaryPhoneNumber()).
name.set(c.getDisplayName());
l.add(cc);
callSeriallyOnIdle(() -> {
cc.photo.set(c.getPhoto());
if(cc.photo.get() != null) {
contactsThread.run(() -> {
String f = cc.name.get() + ".jpg";
try(OutputStream os =
createStorageOutputStream(f)) {
EncodedImage img = EncodedImage.
createFromImage(cc.photo.get(), true);
os.write(img.getImageData());
} catch(IOException err) {
Log.e(err);
}
});
}
});
}
Server
For every entry in the contacts we need to fetch an image, we can use callSeriallyOnIdle to do that. This allows the image loading to occur when the user isn't scrolling
the UI so it won't noticeably impact performance.
true, false, true, false, false);
ArrayList<ChatContact> l = new ArrayList<>();
for(Contact c : contacts) {
ChatContact cc = new ChatContact().
phone.set(c.getPrimaryPhoneNumber()).
name.set(c.getDisplayName());
l.add(cc);
callSeriallyOnIdle(() -> {
cc.photo.set(c.getPhoto());
if(cc.photo.get() != null) {
contactsThread.run(() -> {
String f = cc.name.get() + ".jpg";
try(OutputStream os =
createStorageOutputStream(f)) {
EncodedImage img = EncodedImage.
createFromImage(cc.photo.get(), true);
os.write(img.getImageData());
} catch(IOException err) {
Log.e(err);
}
});
}
});
}
Server
Once we load the photo into the object we save it to storage as well for faster retrieval in the future. This is pretty simplistic code, proper code would have scaled the
image to a uniform size as well. This would have saved memory.
callSeriallyOnIdle(() -> {
cc.photo.set(c.getPhoto());
if(cc.photo.get() != null) {
contactsThread.run(() -> {
String f = cc.name.get() + ".jpg";
try(OutputStream os =
createStorageOutputStream(f)) {
EncodedImage img = EncodedImage.
createFromImage(cc.photo.get(), true);
os.write(img.getImageData());
} catch(IOException err) {
Log.e(err);
}
});
}
});
}
PropertyIndex.storeJSONList("contacts.json", l);
callSerially(() -> contactsCallback.completed(l));
}
public static void findRegisteredUser(String phone,
OnComplete<ChatContact> resultCallback) {
Server
Finally once we are done we save the contacts to the JSON file. This isn’t shown here but the contents of the photo property isn’t stored to the JSON file to keep the size
minimal and loading time short. Once loaded we invoke the callback with the proper argument.
});
}
PropertyIndex.storeJSONList("contacts.json", l);
callSerially(() -> contactsCallback.completed(l));
}
public static void findRegisteredUser(String phone,
OnComplete<ChatContact> resultCallback) {
get("user/findRegisteredUser").
queryParam("phone", phone).
fetchAsPropertyList(res -> {
List l = res.getResponseData();
if(l.size() == 0) {
resultCallback.completed(null);
return;
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void findRegisteredUserById(String id,
OnComplete<ChatContact> resultCallback) {
get("user/findRegisteredUserById").
Server
When we want to contact a user we need to first make sure he’s on our chat platform. For this we have the findRegisteredUser server API. With this API we will receive a
list with one user object or an empty list from the server. This API is asynchronous and we use it to decide whether we can send a message to someone from our
contacts.
resultCallback.completed(null);
return;
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void findRegisteredUserById(String id,
OnComplete<ChatContact> resultCallback) {
get("user/findRegisteredUserById").
queryParam("id", id).
fetchAsPropertyList(res -> {
List l = res.getResponseData();
if(l.size() == 0) {
resultCallback.completed(null);
return;
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void fetchChatList(
OnComplete<List<ChatContact>> contactsCallback) {
Server
This is a similar method that allows us to get a user based on a user ID instead of a phone. If we get a chat message that was sent by a specific user we will need to
know about that user. This method lets us fetch the meta data related to that user.
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void fetchChatList(
OnComplete<List<ChatContact>> contactsCallback) {
fetchContacts(cl -> {
List<ChatContact> response = new ArrayList<>();
for(ChatContact c : cl) {
if(c.lastActivityTime.get() != null) {
response.add(c);
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
Server
The chats we have open with users can be extracted from the list of contacts. Since every contact had its own chat thread.
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void fetchChatList(
OnComplete<List<ChatContact>> contactsCallback) {
fetchContacts(cl -> {
List<ChatContact> response = new ArrayList<>();
for(ChatContact c : cl) {
if(c.lastActivityTime.get() != null) {
response.add(c);
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
Server
So to fetch the chats we see in the main form of the whatsapp UI we need to first fetch the contacts as they might not have been loaded yet.
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void fetchChatList(
OnComplete<List<ChatContact>> contactsCallback) {
fetchContacts(cl -> {
List<ChatContact> response = new ArrayList<>();
for(ChatContact c : cl) {
if(c.lastActivityTime.get() != null) {
response.add(c);
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
Server
We loop over the contacts and if we had activity with that contact we add him to the list in the response
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void fetchChatList(
OnComplete<List<ChatContact>> contactsCallback) {
fetchContacts(cl -> {
List<ChatContact> response = new ArrayList<>();
for(ChatContact c : cl) {
if(c.lastActivityTime.get() != null) {
response.add(c);
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
Server
But before we finish we need to sort the responses based on activity time. The sort method is builtin to the Java collections API. It accepts a comparator which we
represented here as a lambda expression
}
resultCallback.completed((ChatContact)l.get(0));
}, ChatContact.class);
}
public static void fetchChatList(
OnComplete<List<ChatContact>> contactsCallback) {
fetchContacts(cl -> {
List<ChatContact> response = new ArrayList<>();
for(ChatContact c : cl) {
if(c.lastActivityTime.get() != null) {
response.add(c);
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
Server
The comparator compares two objects in the list to one another. It returns a value smaller than 0 to indicate the first value is smaller. zero to indicate the values are
identical and more than 0 to indicate the second value is larger. The simple solution is subtracting the time values to get a valid comparison result.
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
body(messageId).fetchAsString(c -> {});
}
public static void updatePushKey(String key) {
if(user() != null) {
get("user/updatePushKey").
queryParam("id", user().id.get()).
queryParam("key", key).fetchAsString(c -> {});
}
}
}
Server
We saw the ack call earlier. This stands for acknowledgement. We effectively acknowledge that a message was received. If this doesn’t go out the server doesn’t know if
a message reached its destination
}
}
Collections.sort(response,
(ChatContact o1, ChatContact o2) ->
(int)(o1.lastActivityTime.get().getTime() -
o2.lastActivityTime.get().getTime()));
contactsCallback.completed(response);
});
}
public static void ackMessage(String messageId) {
post("user/ackMessage").
body(messageId).fetchAsString(c -> {});
}
public static void updatePushKey(String key) {
if(user() != null) {
get("user/updatePushKey").
queryParam("id", user().id.get()).
queryParam("key", key).fetchAsString(c -> {});
}
}
}
Server
Finally we need this method for push notification. It sends the push key of the device to the server so the server will be able to send push messages to the devices.

More Related Content

PDF
Creating a Whatsapp Clone - Part II.pdf
PDF
Need help on creating code using cart. The output has to show multip.pdf
DOCX
Java Advance 4deleteQuery.PNGJava Advance 4deleteQuerySucc.docx
PDF
May 2010 - RestEasy
PDF
Creating a Facebook Clone - Part XXV - Transcript.pdf
PDF
Include- Modularity using design patterns- Fault tolerance and Compone.pdf
DOCX
Write a java program to create a program that outputs to the command l.docx
PPTX
Servlets
Creating a Whatsapp Clone - Part II.pdf
Need help on creating code using cart. The output has to show multip.pdf
Java Advance 4deleteQuery.PNGJava Advance 4deleteQuerySucc.docx
May 2010 - RestEasy
Creating a Facebook Clone - Part XXV - Transcript.pdf
Include- Modularity using design patterns- Fault tolerance and Compone.pdf
Write a java program to create a program that outputs to the command l.docx
Servlets

Similar to Creating a Whatsapp Clone - Part II - Transcript.pdf (20)

PDF
Hi, I need some one to help me with Design a client-server Chat so.pdf
PDF
PDF
I really need help on this question.Create a program that allows t.pdf
PDF
Manual tecnic sergi_subirats
DOCX
JavaExamples
PDF
Speed up your Web applications with HTML5 WebSockets
ODP
Codemotion appengine
PPTX
Use Windows Azure Service Bus, BizTalk Services, Mobile Services, and BizTalk...
PPT
Distributed Objects and JAVA
PDF
Creating a Facebook Clone - Part XXVII - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part XIV - Transcript.pdf
PPTX
The Full Power of ASP.NET Web API
PDF
Creating a Whatsapp Clone - Part XV - Transcript.pdf
PDF
Creating an Uber Clone - Part XIII - Transcript.pdf
PPTX
Node.js System: The Approach
DOCX
4th semester project report
PDF
JavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
KEY
Integrating Wicket with Java EE 6
DOCX
Laporan multi client
PPTX
Local SQLite Database with Node for beginners
Hi, I need some one to help me with Design a client-server Chat so.pdf
I really need help on this question.Create a program that allows t.pdf
Manual tecnic sergi_subirats
JavaExamples
Speed up your Web applications with HTML5 WebSockets
Codemotion appengine
Use Windows Azure Service Bus, BizTalk Services, Mobile Services, and BizTalk...
Distributed Objects and JAVA
Creating a Facebook Clone - Part XXVII - Transcript.pdf
Creating a Whatsapp Clone - Part XIV - Transcript.pdf
The Full Power of ASP.NET Web API
Creating a Whatsapp Clone - Part XV - Transcript.pdf
Creating an Uber Clone - Part XIII - Transcript.pdf
Node.js System: The Approach
4th semester project report
JavaCro'14 - Building interactive web applications with Vaadin – Peter Lehto
Integrating Wicket with Java EE 6
Laporan multi client
Local SQLite Database with Node for beginners
Ad

More from ShaiAlmog1 (20)

PDF
The Duck Teaches Learn to debug from the masters. Local to production- kill ...
PDF
create-netflix-clone-06-client-ui.pdf
PDF
create-netflix-clone-01-introduction_transcript.pdf
PDF
create-netflix-clone-02-server_transcript.pdf
PDF
create-netflix-clone-04-server-continued_transcript.pdf
PDF
create-netflix-clone-01-introduction.pdf
PDF
create-netflix-clone-06-client-ui_transcript.pdf
PDF
create-netflix-clone-03-server.pdf
PDF
create-netflix-clone-04-server-continued.pdf
PDF
create-netflix-clone-05-client-model_transcript.pdf
PDF
create-netflix-clone-03-server_transcript.pdf
PDF
create-netflix-clone-02-server.pdf
PDF
create-netflix-clone-05-client-model.pdf
PDF
Creating a Whatsapp Clone - Part IX - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part V - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IV - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IV.pdf
PDF
Creating a Whatsapp Clone - Part I - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IX.pdf
PDF
Creating a Whatsapp Clone - Part VI.pdf
The Duck Teaches Learn to debug from the masters. Local to production- kill ...
create-netflix-clone-06-client-ui.pdf
create-netflix-clone-01-introduction_transcript.pdf
create-netflix-clone-02-server_transcript.pdf
create-netflix-clone-04-server-continued_transcript.pdf
create-netflix-clone-01-introduction.pdf
create-netflix-clone-06-client-ui_transcript.pdf
create-netflix-clone-03-server.pdf
create-netflix-clone-04-server-continued.pdf
create-netflix-clone-05-client-model_transcript.pdf
create-netflix-clone-03-server_transcript.pdf
create-netflix-clone-02-server.pdf
create-netflix-clone-05-client-model.pdf
Creating a Whatsapp Clone - Part IX - Transcript.pdf
Creating a Whatsapp Clone - Part V - Transcript.pdf
Creating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Whatsapp Clone - Part IV.pdf
Creating a Whatsapp Clone - Part I - Transcript.pdf
Creating a Whatsapp Clone - Part IX.pdf
Creating a Whatsapp Clone - Part VI.pdf
Ad

Recently uploaded (20)

PDF
Electronic commerce courselecture one. Pdf
PPTX
MYSQL Presentation for SQL database connectivity
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PPTX
20250228 LYD VKU AI Blended-Learning.pptx
PPTX
Cloud computing and distributed systems.
PDF
Diabetes mellitus diagnosis method based random forest with bat algorithm
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
Machine learning based COVID-19 study performance prediction
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PPTX
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
PDF
NewMind AI Weekly Chronicles - August'25 Week I
PDF
7 ChatGPT Prompts to Help You Define Your Ideal Customer Profile.pdf
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
Empathic Computing: Creating Shared Understanding
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PPTX
Spectroscopy.pptx food analysis technology
PDF
Advanced methodologies resolving dimensionality complications for autism neur...
Electronic commerce courselecture one. Pdf
MYSQL Presentation for SQL database connectivity
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
Reach Out and Touch Someone: Haptics and Empathic Computing
Agricultural_Statistics_at_a_Glance_2022_0.pdf
20250228 LYD VKU AI Blended-Learning.pptx
Cloud computing and distributed systems.
Diabetes mellitus diagnosis method based random forest with bat algorithm
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
The Rise and Fall of 3GPP – Time for a Sabbatical?
Machine learning based COVID-19 study performance prediction
Building Integrated photovoltaic BIPV_UPV.pdf
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
NewMind AI Weekly Chronicles - August'25 Week I
7 ChatGPT Prompts to Help You Define Your Ideal Customer Profile.pdf
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Empathic Computing: Creating Shared Understanding
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
Spectroscopy.pptx food analysis technology
Advanced methodologies resolving dimensionality complications for autism neur...

Creating a Whatsapp Clone - Part II - Transcript.pdf

  • 1. Creating a WhatsApp Clone - Part II We’ll jump into the client functionality from the server connectivity class. I won’t start with the UI and build everything up but instead go through the code relatively quickly as I’m assuming you’ve gone through the longer explanations in the previous modules.
  • 2. * need additional information or have any questions. */ package com.codename1.whatsapp.model; import com.codename1.contacts.Contact; import com.codename1.io.JSONParser; import com.codename1.io.Log; import com.codename1.io.Preferences; import com.codename1.io.Util; import com.codename1.io.rest.RequestBuilder; import com.codename1.io.rest.Response; import com.codename1.io.rest.Rest; import com.codename1.io.websocket.WebSocket; import com.codename1.properties.PropertyIndex; import static com.codename1.ui.CN.*; import com.codename1.ui.Display; import com.codename1.ui.EncodedImage; import com.codename1.util.EasyThread; import com.codename1.util.OnComplete; import com.codename1.util.regex.StringReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; Server Like before the Server class abstracts the backend. I’ll soon go into the details of the other classes in this package which are property business object abstractions. As a reminder notice that I import the CN class so I can use shorthand syntax for various API’s. I do this in almost all files in the project.
  • 3. import com.codename1.ui.Display; import com.codename1.ui.EncodedImage; import com.codename1.util.EasyThread; import com.codename1.util.OnComplete; import com.codename1.util.regex.StringReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; Server Right now the debug environment points at the local host but in order to work with devices this will need to point at an actual URL or IP address
  • 4. import com.codename1.ui.Display; import com.codename1.ui.EncodedImage; import com.codename1.util.EasyThread; import com.codename1.util.OnComplete; import com.codename1.util.regex.StringReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; Server As I mentioned before we’ll store the data as JSON in storage. The file names don’t have to end in “.json”, I just did that for our convenience.
  • 5. import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; private static boolean connected; private static List<ChatMessage> messageQueue; public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); Server This is a property business object we’ll discuss soon. We use it to represent all our contacts and outselves
  • 6. import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; private static boolean connected; private static List<ChatMessage> messageQueue; public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); Server This is the current websocket connection, we need this to be global as we will disconnect from the server when the app is minimized. That’s important otherwise battery saving code might kill the app
  • 7. import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; private static boolean connected; private static List<ChatMessage> messageQueue; public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); Server This flag indicates whether he websocket is connected which saves us from asking the connection if it’s still active.
  • 8. import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; private static boolean connected; private static List<ChatMessage> messageQueue; public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); Server If we aren't connected new messages go into the message queue and will go out when we reconnect.
  • 9. import java.util.List; import java.util.Map; public class Server { private static final String SERVER_URL = "http://localhost:8080/"; private static final String WEBSOCKER_URL = "ws://localhost:8080/socket"; private static final String USER_FILE_NAME = "user.json"; private static final String MESSAGE_QUEUE_FILE_NAME = "message_queue.json"; private static ChatContact currentUser; private static WebSocket connection; private static boolean connected; private static List<ChatMessage> messageQueue; public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); Server The user logged into the app is global
  • 10. public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) { messageQueue = new ChatMessage().getPropertyIndex(). loadJSONList(MESSAGE_QUEUE_FILE_NAME); } if(existsInStorage("contacts.json")) { contactCache = new ChatContact().getPropertyIndex(). loadJSONList("contacts.json"); } else { contactCache = new ArrayList<>(); } } else { contactCache = new ArrayList<>(); } } Server The init method is invoked when the app is loaded, it loads the global data from storage and sets the variable values. Normally there should be data here with the special case of the first activation.
  • 11. public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) { messageQueue = new ChatMessage().getPropertyIndex(). loadJSONList(MESSAGE_QUEUE_FILE_NAME); } if(existsInStorage("contacts.json")) { contactCache = new ChatContact().getPropertyIndex(). loadJSONList("contacts.json"); } else { contactCache = new ArrayList<>(); } } else { contactCache = new ArrayList<>(); } } Server If this is the first activation before receiving the validation SMS this file won’t exist. In that case we’ll just initialize the contact cache as an empty list and be on our way.
  • 12. public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) { messageQueue = new ChatMessage().getPropertyIndex(). loadJSONList(MESSAGE_QUEUE_FILE_NAME); } if(existsInStorage("contacts.json")) { contactCache = new ChatContact().getPropertyIndex(). loadJSONList("contacts.json"); } else { contactCache = new ArrayList<>(); } } else { contactCache = new ArrayList<>(); } } Server Assuming we are logged in we can load the data for the current user this is pretty easy to do for property business objects.
  • 13. public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) { messageQueue = new ChatMessage().getPropertyIndex(). loadJSONList(MESSAGE_QUEUE_FILE_NAME); } if(existsInStorage("contacts.json")) { contactCache = new ChatContact().getPropertyIndex(). loadJSONList("contacts.json"); } else { contactCache = new ArrayList<>(); } } else { contactCache = new ArrayList<>(); } } Server If there are messages in the message queue we need to load them as well. This can happen if the user sends a message without connectivity and the app is killed
  • 14. public static ChatContact user() { return currentUser; } public static void init() { if(existsInStorage(USER_FILE_NAME)) { currentUser = new ChatContact(); currentUser.getPropertyIndex().loadJSON(USER_FILE_NAME); if(existsInStorage(MESSAGE_QUEUE_FILE_NAME)) { messageQueue = new ChatMessage().getPropertyIndex(). loadJSONList(MESSAGE_QUEUE_FILE_NAME); } if(existsInStorage("contacts.json")) { contactCache = new ChatContact().getPropertyIndex(). loadJSONList("contacts.json"); } else { contactCache = new ArrayList<>(); } } else { contactCache = new ArrayList<>(); } } Server Contacts are cached here, the contacts essentially contain everything in the app. This might be a bit wasteful to store all the data in this way but it should work reasonably even for relatively large datasets
  • 15. } else { contactCache = new ArrayList<>(); } } public static void flushMessageQueue() { if(connected && messageQueue != null && messageQueue.size() > 0) { for(ChatMessage m : messageQueue) { connection.send(m.getPropertyIndex().toJSON()); } messageQueue.clear(); } } private static RequestBuilder post(String u) { RequestBuilder r = Rest.post(SERVER_URL + u).jsonContent(); if(currentUser != null && currentUser.token.get() != null) { r.header("auth", currentUser.token.get()); } return r; } private static RequestBuilder get(String u) { RequestBuilder r = Rest.get(SERVER_URL + u).jsonContent(); Server This method sends the content of the message queue, it’s invoked when we go back online
  • 16. } messageQueue.clear(); } } private static RequestBuilder post(String u) { RequestBuilder r = Rest.post(SERVER_URL + u).jsonContent(); if(currentUser != null && currentUser.token.get() != null) { r.header("auth", currentUser.token.get()); } return r; } private static RequestBuilder get(String u) { RequestBuilder r = Rest.get(SERVER_URL + u).jsonContent(); if(currentUser != null && currentUser.token.get() != null) { r.header("auth", currentUser.token.get()); } return r; } public static void login(OnComplete<ChatContact> c) { post("user/login"). body(currentUser).fetchAsProperties( Server These methods are shorthand for get and post methods of the Rest API. They force JSON usage and add the auth header which most of the server side API’s will need. That lets us write shorter code.
  • 17. r.header("auth", currentUser.token.get()); } return r; } public static void login(OnComplete<ChatContact> c) { post("user/login"). body(currentUser).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); currentUser. getPropertyIndex(). storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static void signup(ChatContact user, OnComplete<ChatContact> c) { post("user/signup"). body(user).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); Server The login method is the first server side method. It doesn’t do much, it sends the current user to the server then saves the returned instance of that user. This allows us to refresh user data from the server.
  • 18. r.header("auth", currentUser.token.get()); } return r; } public static void login(OnComplete<ChatContact> c) { post("user/login"). body(currentUser).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); currentUser. getPropertyIndex(). storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static void signup(ChatContact user, OnComplete<ChatContact> c) { post("user/signup"). body(user).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); Server We pass the current user as the body in an argument, notice I can pass the property business object directly and it will be converted to JSON.
  • 19. r.header("auth", currentUser.token.get()); } return r; } public static void login(OnComplete<ChatContact> c) { post("user/login"). body(currentUser).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); currentUser. getPropertyIndex(). storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static void signup(ChatContact user, OnComplete<ChatContact> c) { post("user/signup"). body(user).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); Server In the response we read the user replace the current instance and save it to disk.
  • 20. c.completed(currentUser); }, ChatContact.class); } public static void signup(ChatContact user, OnComplete<ChatContact> c) { post("user/signup"). body(user).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); currentUser. getPropertyIndex(). storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static void update(OnComplete<ChatContact> c) { post("user/update"). body(currentUser).fetchAsProperties( res -> { Server Signup is very similar to login, in fact it’s identical. However, after signup is complete you still don’t have anything since we need to verify the user, so lets skip down to that
  • 21. storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static boolean verify(String code) { Response<String> result = get("user/verify"). queryParam("userId", currentUser.id.get()). queryParam("code", code). getAsString(); return "OK".equals(result.getResponseData()); } public static void sendMessage(ChatMessage m, ChatContact cont) { cont.lastActivityTime.set(new Date()); cont.chats.add(m); saveContacts(); if(connected) { post("user/sendMessage"). body(m). fetchAsProperties(e -> { cont.chats.remove(m); cont.chats.add((ChatMessage)e.getResponseData()); Server On the server, signup triggers an SMS which we need to intercept. We then need to send the SMS code via this API. Only after this method returns OK our user becomes valid.
  • 22. storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static void update(OnComplete<ChatContact> c) { post("user/update"). body(currentUser).fetchAsProperties( res -> { currentUser = (ChatContact)res.getResponseData(); currentUser. getPropertyIndex(). storeJSON(USER_FILE_NAME); c.completed(currentUser); }, ChatContact.class); } public static boolean verify(String code) { Response<String> result = get("user/verify"). queryParam("userId", currentUser.id.get()). queryParam("code", code). getAsString(); Server Update is practically identical to the two other methods but sends the updated data from the client to the server. It isn’t interesting.
  • 23. getAsString(); return "OK".equals(result.getResponseData()); } public static void sendMessage(ChatMessage m, ChatContact cont) { cont.lastActivityTime.set(new Date()); cont.chats.add(m); saveContacts(); if(connected) { post("user/sendMessage"). body(m). fetchAsProperties(e -> { cont.chats.remove(m); cont.chats.add((ChatMessage)e.getResponseData()); saveContacts(); }, ChatMessage.class); //connection.send(m.getPropertyIndex().toJSON()); } else { if(messageQueue == null) { messageQueue = new ArrayList<>(); } messageQueue.add(m); PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME, messageQueue); Server send message is probably the most important method here. It delivers a message to the server and saves it into the JSON storage.
  • 24. getAsString(); return "OK".equals(result.getResponseData()); } public static void sendMessage(ChatMessage m, ChatContact cont) { cont.lastActivityTime.set(new Date()); cont.chats.add(m); saveContacts(); if(connected) { post("user/sendMessage"). body(m). fetchAsProperties(e -> { cont.chats.remove(m); cont.chats.add((ChatMessage)e.getResponseData()); saveContacts(); }, ChatMessage.class); //connection.send(m.getPropertyIndex().toJSON()); } else { if(messageQueue == null) { messageQueue = new ArrayList<>(); } messageQueue.add(m); PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME, messageQueue); Server Here we save the time in which a specific contact last chatted, this allows us to sort the contacts based on the time a specific contact last chatted with us
  • 25. getAsString(); return "OK".equals(result.getResponseData()); } public static void sendMessage(ChatMessage m, ChatContact cont) { cont.lastActivityTime.set(new Date()); cont.chats.add(m); saveContacts(); if(connected) { post("user/sendMessage"). body(m). fetchAsProperties(e -> { cont.chats.remove(m); cont.chats.add((ChatMessage)e.getResponseData()); saveContacts(); }, ChatMessage.class); //connection.send(m.getPropertyIndex().toJSON()); } else { if(messageQueue == null) { messageQueue = new ArrayList<>(); } messageQueue.add(m); PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME, messageQueue); Server This sends the message using a webservice. The message body is submitted as a ChatMessage business object which is implicitly translated to JSON
  • 26. getAsString(); return "OK".equals(result.getResponseData()); } public static void sendMessage(ChatMessage m, ChatContact cont) { cont.lastActivityTime.set(new Date()); cont.chats.add(m); saveContacts(); if(connected) { post("user/sendMessage"). body(m). fetchAsProperties(e -> { cont.chats.remove(m); cont.chats.add((ChatMessage)e.getResponseData()); saveContacts(); }, ChatMessage.class); //connection.send(m.getPropertyIndex().toJSON()); } else { if(messageQueue == null) { messageQueue = new ArrayList<>(); } messageQueue.add(m); PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME, messageQueue); Server Initially I sent messages via the websocket but there wasn’t a big benefit to doing that. I kept that code in place for reference. The advantage of using a websocket is mostly in the server side where calls are seamlessly translated.
  • 27. public static void sendMessage(ChatMessage m, ChatContact cont) { cont.lastActivityTime.set(new Date()); cont.chats.add(m); saveContacts(); if(connected) { post("user/sendMessage"). body(m). fetchAsProperties(e -> { cont.chats.remove(m); cont.chats.add((ChatMessage)e.getResponseData()); saveContacts(); }, ChatMessage.class); //connection.send(m.getPropertyIndex().toJSON()); } else { if(messageQueue == null) { messageQueue = new ArrayList<>(); } messageQueue.add(m); PropertyIndex.storeJSONList(MESSAGE_QUEUE_FILE_NAME, messageQueue); } } Server If we are offline the message is added to the message queue and the content of the queue is saved
  • 28. messageQueue); } } public static void bindMessageListener(final ServerMessages callback) { connection = new WebSocket(WEBSOCKER_URL) { @Override protected void onOpen() { connected = true; long lastMessageTime = Preferences.get("LastReceivedMessage", (long)0); send("{"t":"init","tok":"" + currentUser.token.get() + "","time":" + lastMessageTime + "}"); callSerially(() -> callback.connected()); final WebSocket w = this; new Thread() { public void run() { Util.sleep(80000); while(connection == w) { // keep-alive message every 80 seconds to avoid // cloudflare killing of the connection Server This method binds the websocket to the server and handles incoming/outgoing messages over the websocket connection. This is a pretty big method because of the inner class within it, but it’s relatively simple as the inner class is mostly trivial. The bind method receives a callback interface for various application level events. E.g. when a message is received we’d like to update the UI to indicate that. We can do that via the callback interface without getting all of that logic into the server class.
  • 29. messageQueue); } } public static void bindMessageListener(final ServerMessages callback) { connection = new WebSocket(WEBSOCKER_URL) { @Override protected void onOpen() { connected = true; long lastMessageTime = Preferences.get("LastReceivedMessage", (long)0); send("{"t":"init","tok":"" + currentUser.token.get() + "","time":" + lastMessageTime + "}"); callSerially(() -> callback.connected()); final WebSocket w = this; new Thread() { public void run() { Util.sleep(80000); while(connection == w) { // keep-alive message every 80 seconds to avoid // cloudflare killing of the connection Server Here we create a subclass of websocket and override all the relevant callback methods.
  • 30. ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Log.e(err); throw new RuntimeException(err.toString()); } } @Override protected void onMessage(byte[] message) { } @Override protected void onError(Exception ex) { Log.e(ex); } }; connection.autoReconnect(5000); connection.connect(); } private static void updateMessage(ChatMessage m) { for(ChatContact c : contactCache) { Server Skipping to the end of the method we can see the connection call and also the autoReconnect method which automatically tries to reconnect every 5 seconds if we lost the websocket connection.
  • 31. public static void bindMessageListener(final ServerMessages callback) { connection = new WebSocket(WEBSOCKER_URL) { @Override protected void onOpen() { connected = true; long lastMessageTime = Preferences.get("LastReceivedMessage", (long)0); send("{"t":"init","tok":"" + currentUser.token.get() + "","time":" + lastMessageTime + "}"); callSerially(() -> callback.connected()); final WebSocket w = this; new Thread() { public void run() { Util.sleep(80000); while(connection == w) { // keep-alive message every 80 seconds to avoid // cloudflare killing of the connection // https://community.cloudflare.com/t/cloudflare- websocket-timeout/5865/3 send("{"t":"ping"}"); Util.sleep(80000); Server Let’s go back to the callback methods starting with onOpen(). This method is invoked when the connection is established. Once this is established we can start making websocket calls and receiving messages.
  • 32. public static void bindMessageListener(final ServerMessages callback) { connection = new WebSocket(WEBSOCKER_URL) { @Override protected void onOpen() { connected = true; long lastMessageTime = Preferences.get("LastReceivedMessage", (long)0); send("{"t":"init","tok":"" + currentUser.token.get() + "","time":" + lastMessageTime + "}"); callSerially(() -> callback.connected()); final WebSocket w = this; new Thread() { public void run() { Util.sleep(80000); while(connection == w) { // keep-alive message every 80 seconds to avoid // cloudflare killing of the connection // https://community.cloudflare.com/t/cloudflare- websocket-timeout/5865/3 send("{"t":"ping"}"); Util.sleep(80000); Server We start by sending an init message, This is a simple JSON message that provides the authorization token for the current user and the time of the last message received. This means the server now knows we are connected and knows the time of the message we last received, it means that if the server has messages pending it can send them now.
  • 33. public static void bindMessageListener(final ServerMessages callback) { connection = new WebSocket(WEBSOCKER_URL) { @Override protected void onOpen() { connected = true; long lastMessageTime = Preferences.get("LastReceivedMessage", (long)0); send("{"t":"init","tok":"" + currentUser.token.get() + "","time":" + lastMessageTime + "}"); callSerially(() -> callback.connected()); final WebSocket w = this; new Thread() { public void run() { Util.sleep(80000); while(connection == w) { // keep-alive message every 80 seconds to avoid // cloudflare killing of the connection // https://community.cloudflare.com/t/cloudflare- websocket-timeout/5865/3 send("{"t":"ping"}"); Util.sleep(80000); Server Next we send an event that we are connected, notice I used callSerially to send it on the EDT. Since these events will most likely handle GUI this makes sense.
  • 34. long lastMessageTime = Preferences.get("LastReceivedMessage", (long)0); send("{"t":"init","tok":"" + currentUser.token.get() + "","time":" + lastMessageTime + "}"); callSerially(() -> callback.connected()); final WebSocket w = this; new Thread() { public void run() { Util.sleep(80000); while(connection == w) { // keep-alive message every 80 seconds to avoid // cloudflare killing of the connection // https://community.cloudflare.com/t/cloudflare- websocket-timeout/5865/3 send("{"t":"ping"}"); Util.sleep(80000); } } }.start(); } @Override Server Finally, we open a thread to send a ping message every 80 seconds. This is redundant for most users and you can remove that code if you don’t use cloudflare. However, if you do then cloudflare closes connections after 100 seconds of inactivity. That way the connection isn't closed as cloudflare sees that it’s active. 
 Cloudflare is a content delivery network we use for our web properties. It helps scale and protect your domain but it isn't essential for this specific deployment. Still I chose to keep that code in because this took us a while to discover and might be a stumbling block for you as well.
  • 35. } }.start(); } @Override protected void onClose(int statusCode, String reason) { connected = false; callSerially(() -> callback.disconnected()); } @Override protected void onMessage(String message) { try { StringReader r = new StringReader(message); JSONParser jp = new JSONParser(); JSONParser.setUseBoolean(true); JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { Server When a connection is closed we call the event (again on the EDT) and mark the connected flag appropriately.
  • 36. connected = false; callSerially(() -> callback.disconnected()); } @Override protected void onMessage(String message) { try { StringReader r = new StringReader(message); JSONParser jp = new JSONParser(); JSONParser.setUseBoolean(true); JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); Server All the messages in the app are text based messages so we use this version of the message callback event to handle incoming messages.
  • 37. connected = false; callSerially(() -> callback.disconnected()); } @Override protected void onMessage(String message) { try { StringReader r = new StringReader(message); JSONParser jp = new JSONParser(); JSONParser.setUseBoolean(true); JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); Server Technically the messages are JSON strings, so we convert the String to a reader object. Then we parse the message and pass the result into the property business object. This can actually be written in a slightly more concise way with the fromJSON() method. However, that method didn't exist when I wrote this code.
  • 38. JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); return; } Preferences.set("LastReceivedMessage", c.time.get().getTime()); updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Server Now that we parsed the object we need to decide what to do with it. We do that on the EDT since the results would process to impact the UI
  • 39. JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); return; } Preferences.set("LastReceivedMessage", c.time.get().getTime()); updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Server The typing flag allows us to send an event that a user is typing, I didn't fully implement this feature but the callback and event behavior is correct.
  • 40. JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); return; } Preferences.set("LastReceivedMessage", c.time.get().getTime()); updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Server Another feature that I didn’t completely finish is the viewed by feature. Here we can process an event indicating there was a change in the list of people who saw a specific message
  • 41. JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); return; } Preferences.set("LastReceivedMessage", c.time.get().getTime()); updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Server If it’s not one of those then it’s an actual message. We need to start by updating the last received message time. I’ll discuss update message soon, it effectively stores the message.
  • 42. JSONParser.setUseLongs(true); Map m = jp.parseJSON(r); ChatMessage c = new ChatMessage(); c.getPropertyIndex(). populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); return; } Preferences.set("LastReceivedMessage", c.time.get().getTime()); updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Server ackMessage acknowledges to the server that the message was received. This is important otherwise a message might be resent to make sure we received it.
  • 43. populateFromMap(m, ChatMessage.class); callSerially(() -> { if(c.typing.get() != null && c.typing.getBoolean()) { callback.userTyping(c.authorId.get()); return; } if(c.viewedBy.size() > 0) { callback.messageViewed(message, c.viewedBy.asList()); return; } Preferences.set("LastReceivedMessage", c.time.get().getTime()); updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Log.e(err); throw new RuntimeException(err.toString()); } } Server Finally we invoke the message received callback. Since we are already within a call serially we don’t need to wrap this too.
  • 44. updateMessage(c); ackMessage(c.id.get()); callback.messageReceived(c); }); } catch(IOException err) { Log.e(err); throw new RuntimeException(err.toString()); } } @Override protected void onMessage(byte[] message) { } @Override protected void onError(Exception ex) { Log.e(ex); } }; connection.autoReconnect(5000); connection.connect(); } Server We don't use binary messages and most errors would be resolved by autoReconnect. Still it’s important to at least log the errors.
  • 45. } }; connection.autoReconnect(5000); connection.connect(); } private static void updateMessage(ChatMessage m) { for(ChatContact c : contactCache) { if(c.id.get() != null && c.id.get().equals(m.authorId.get())) { c.lastActivityTime.set(new Date()); c.chats.add(m); saveContacts(); return; } } findRegisteredUserById(m.authorId.get(), cc -> { contactCache.add(cc); cc.chats.add(m); saveContacts(); }); } public static void closeWebsocketConnection() { Server The update method is invoked to update a message in the chat.
  • 46. } }; connection.autoReconnect(5000); connection.connect(); } private static void updateMessage(ChatMessage m) { for(ChatContact c : contactCache) { if(c.id.get() != null && c.id.get().equals(m.authorId.get())) { c.lastActivityTime.set(new Date()); c.chats.add(m); saveContacts(); return; } } findRegisteredUserById(m.authorId.get(), cc -> { contactCache.add(cc); cc.chats.add(m); saveContacts(); }); } public static void closeWebsocketConnection() { Server First we loop over the existing contacts and try to find the right one. Once we find that contact we can add the message to the contact
  • 47. } }; connection.autoReconnect(5000); connection.connect(); } private static void updateMessage(ChatMessage m) { for(ChatContact c : contactCache) { if(c.id.get() != null && c.id.get().equals(m.authorId.get())) { c.lastActivityTime.set(new Date()); c.chats.add(m); saveContacts(); return; } } findRegisteredUserById(m.authorId.get(), cc -> { contactCache.add(cc); cc.chats.add(m); saveContacts(); }); } public static void closeWebsocketConnection() { Server The find method finds that contact and we add a new message into the database
  • 48. contactCache.add(cc); cc.chats.add(m); saveContacts(); }); } public static void closeWebsocketConnection() { if(connection != null) { connection.close(); connection = null; } } public static void saveContacts() { if(contactCache != null && contactsThread != null) { contactsThread.run(() -> { PropertyIndex.storeJSONList("contacts.json", contactCache); }); } } private static List<ChatContact> contactCache; private static EasyThread contactsThread; Server This method closes the websocket connection. It’s something we need to do when the app is suspended so the OS doesn’t kill the app. We’ll discuss this when talking about the lifecycle methods later
  • 49. }); } public static void closeWebsocketConnection() { if(connection != null) { connection.close(); connection = null; } } public static void saveContacts() { if(contactCache != null && contactsThread != null) { contactsThread.run(() -> { PropertyIndex.storeJSONList("contacts.json", contactCache); }); } } private static List<ChatContact> contactCache; private static EasyThread contactsThread; public static void fetchContacts( OnComplete<List<ChatContact>> contactsCallback) { if(contactsThread == null) { Server The contacts are saved on the contacts thread, we use this helper method to go into the helper thread to prevent race conditions
  • 50. }); } } private static List<ChatContact> contactCache; private static EasyThread contactsThread; public static void fetchContacts( OnComplete<List<ChatContact>> contactsCallback) { if(contactsThread == null) { contactsThread = EasyThread.start("Contacts Thread"); contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(!contactsThread.isThisIt()) { contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(contactCache != null) { callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). Server Fetch contacts loads the contacts from the JSON list or the device contacts. Since this can be an expensive operation we do it on a separate contacts thread which is an easy thread.
  • 51. }); } } private static List<ChatContact> contactCache; private static EasyThread contactsThread; public static void fetchContacts( OnComplete<List<ChatContact>> contactsCallback) { if(contactsThread == null) { contactsThread = EasyThread.start("Contacts Thread"); contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(!contactsThread.isThisIt()) { contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(contactCache != null) { callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). Server Easy threads let us send tasks to the thread, similarly to callSerially on the EDT. Here we lazily create the easy thread and then run fetchContacts on that thread assuming the current easy thread is null.
  • 52. }); } } private static List<ChatContact> contactCache; private static EasyThread contactsThread; public static void fetchContacts( OnComplete<List<ChatContact>> contactsCallback) { if(contactsThread == null) { contactsThread = EasyThread.start("Contacts Thread"); contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(!contactsThread.isThisIt()) { contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(contactCache != null) { callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). Server If the thread already exists we check whether we already are on the easy thread. Assuming we aren’t on the easy thread we call this method again on the thread and return. All the following lines are now guaranteed to run on one thread which is the easy thread. As such they are effectively thread safe and won’t slow down the EDT unless we do something that’s very CPU intensive.
  • 53. }); } } private static List<ChatContact> contactCache; private static EasyThread contactsThread; public static void fetchContacts( OnComplete<List<ChatContact>> contactsCallback) { if(contactsThread == null) { contactsThread = EasyThread.start("Contacts Thread"); contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(!contactsThread.isThisIt()) { contactsThread.run(() -> fetchContacts(contactsCallback)); return; } if(contactCache != null) { callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). Server If we already have the data we use callSeriallyOnIdle. This is a slow version of callSerially that waits for the EDT to reach idle state. This is important for performance. A regular callSerially might occur when the system is animating or in need of resources. If we want to do something expensive or slow it might cause chocking of the UI. callSeriallyOnIdle will delay the callSerially to a point where there are no pending animations or user interaction, this means that there is enough CPU to perform the operation.
  • 54. callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). getPropertyIndex(). loadJSONList("contacts.json"); callSerially(() -> contactsCallback.completed(contactCache)); for(ChatContact c : contactCache) { if(existsInStorage(c.name.get() + ".jpg")) { String f = c.name.get() + ".jpg"; try (InputStream is = createStorageInputStream(f)) { c.photo.set(EncodedImage.create(is)); } catch(IOException err) { Log.e(err); } } } return; } Contact[] contacts = Display.getInstance().getAllContacts(true, Server If we have a JSON file for the contacts we use that as a starting point. This allows us to store all the data in one place and mutate the data as we see fit. We keep the contacts in a contacts cache map which enables fast access at the tradeoff of some RAM. This isn’t too much since we store the thumbnails as external jpegs.
  • 55. callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). getPropertyIndex(). loadJSONList("contacts.json"); callSerially(() -> contactsCallback.completed(contactCache)); for(ChatContact c : contactCache) { if(existsInStorage(c.name.get() + ".jpg")) { String f = c.name.get() + ".jpg"; try (InputStream is = createStorageInputStream(f)) { c.photo.set(EncodedImage.create(is)); } catch(IOException err) { Log.e(err); } } } return; } Contact[] contacts = Display.getInstance().getAllContacts(true, Server Once we loaded the core JSON data we use callSerially to send the event of loading completion, but we aren’t done yet
  • 56. callSeriallyOnIdle(() -> contactsCallback.completed(contactCache)); return; } if(existsInStorage("contacts.json")) { contactCache = new ChatContact(). getPropertyIndex(). loadJSONList("contacts.json"); callSerially(() -> contactsCallback.completed(contactCache)); for(ChatContact c : contactCache) { if(existsInStorage(c.name.get() + ".jpg")) { String f = c.name.get() + ".jpg"; try (InputStream is = createStorageInputStream(f)) { c.photo.set(EncodedImage.create(is)); } catch(IOException err) { Log.e(err); } } } return; } Contact[] contacts = Display.getInstance().getAllContacts(true, Server We loop over the contacts we loaded and check if there is an image file matching the contact name. Assuming there is we load it on the contacts thread and set it to the contact. This will fire an event on the property object and trigger a repaint asynchronously.
  • 57. } } } return; } Contact[] contacts = Display.getInstance().getAllContacts(true, true, false, true, false, false); ArrayList<ChatContact> l = new ArrayList<>(); for(Contact c : contacts) { ChatContact cc = new ChatContact(). phone.set(c.getPrimaryPhoneNumber()). name.set(c.getDisplayName()); l.add(cc); callSeriallyOnIdle(() -> { cc.photo.set(c.getPhoto()); if(cc.photo.get() != null) { contactsThread.run(() -> { String f = cc.name.get() + ".jpg"; try(OutputStream os = createStorageOutputStream(f)) { EncodedImage img = EncodedImage. createFromImage(cc.photo.get(), true); os.write(img.getImageData()); Server If we don’t have a JSON file we need to create it and the place to start is the contacts on the device. getAllContacts fetches all the device contacts. The first argument is true if we only want contacts that have phone numbers associated with them. This is true as we don’t need contacts without phone numbers. The next few values indicate the attributes we need from the contacts database, we don’t need most of the attributes. We only fetch the full name and phone number. The reason for this is performance, fetching all attributes can be very expensive even on a fast device.
  • 58. return; } Contact[] contacts = Display.getInstance().getAllContacts(true, true, false, true, false, false); ArrayList<ChatContact> l = new ArrayList<>(); for(Contact c : contacts) { ChatContact cc = new ChatContact(). phone.set(c.getPrimaryPhoneNumber()). name.set(c.getDisplayName()); l.add(cc); callSeriallyOnIdle(() -> { cc.photo.set(c.getPhoto()); if(cc.photo.get() != null) { contactsThread.run(() -> { String f = cc.name.get() + ".jpg"; try(OutputStream os = createStorageOutputStream(f)) { EncodedImage img = EncodedImage. createFromImage(cc.photo.get(), true); os.write(img.getImageData()); } catch(IOException err) { Log.e(err); } Server Next we loop over each contact and add it to the list of contacts. We convert the builtin Contact object to ChatContact in the process.
  • 59. true, false, true, false, false); ArrayList<ChatContact> l = new ArrayList<>(); for(Contact c : contacts) { ChatContact cc = new ChatContact(). phone.set(c.getPrimaryPhoneNumber()). name.set(c.getDisplayName()); l.add(cc); callSeriallyOnIdle(() -> { cc.photo.set(c.getPhoto()); if(cc.photo.get() != null) { contactsThread.run(() -> { String f = cc.name.get() + ".jpg"; try(OutputStream os = createStorageOutputStream(f)) { EncodedImage img = EncodedImage. createFromImage(cc.photo.get(), true); os.write(img.getImageData()); } catch(IOException err) { Log.e(err); } }); } }); } Server For every entry in the contacts we need to fetch an image, we can use callSeriallyOnIdle to do that. This allows the image loading to occur when the user isn't scrolling the UI so it won't noticeably impact performance.
  • 60. true, false, true, false, false); ArrayList<ChatContact> l = new ArrayList<>(); for(Contact c : contacts) { ChatContact cc = new ChatContact(). phone.set(c.getPrimaryPhoneNumber()). name.set(c.getDisplayName()); l.add(cc); callSeriallyOnIdle(() -> { cc.photo.set(c.getPhoto()); if(cc.photo.get() != null) { contactsThread.run(() -> { String f = cc.name.get() + ".jpg"; try(OutputStream os = createStorageOutputStream(f)) { EncodedImage img = EncodedImage. createFromImage(cc.photo.get(), true); os.write(img.getImageData()); } catch(IOException err) { Log.e(err); } }); } }); } Server Once we load the photo into the object we save it to storage as well for faster retrieval in the future. This is pretty simplistic code, proper code would have scaled the image to a uniform size as well. This would have saved memory.
  • 61. callSeriallyOnIdle(() -> { cc.photo.set(c.getPhoto()); if(cc.photo.get() != null) { contactsThread.run(() -> { String f = cc.name.get() + ".jpg"; try(OutputStream os = createStorageOutputStream(f)) { EncodedImage img = EncodedImage. createFromImage(cc.photo.get(), true); os.write(img.getImageData()); } catch(IOException err) { Log.e(err); } }); } }); } PropertyIndex.storeJSONList("contacts.json", l); callSerially(() -> contactsCallback.completed(l)); } public static void findRegisteredUser(String phone, OnComplete<ChatContact> resultCallback) { Server Finally once we are done we save the contacts to the JSON file. This isn’t shown here but the contents of the photo property isn’t stored to the JSON file to keep the size minimal and loading time short. Once loaded we invoke the callback with the proper argument.
  • 62. }); } PropertyIndex.storeJSONList("contacts.json", l); callSerially(() -> contactsCallback.completed(l)); } public static void findRegisteredUser(String phone, OnComplete<ChatContact> resultCallback) { get("user/findRegisteredUser"). queryParam("phone", phone). fetchAsPropertyList(res -> { List l = res.getResponseData(); if(l.size() == 0) { resultCallback.completed(null); return; } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void findRegisteredUserById(String id, OnComplete<ChatContact> resultCallback) { get("user/findRegisteredUserById"). Server When we want to contact a user we need to first make sure he’s on our chat platform. For this we have the findRegisteredUser server API. With this API we will receive a list with one user object or an empty list from the server. This API is asynchronous and we use it to decide whether we can send a message to someone from our contacts.
  • 63. resultCallback.completed(null); return; } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void findRegisteredUserById(String id, OnComplete<ChatContact> resultCallback) { get("user/findRegisteredUserById"). queryParam("id", id). fetchAsPropertyList(res -> { List l = res.getResponseData(); if(l.size() == 0) { resultCallback.completed(null); return; } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void fetchChatList( OnComplete<List<ChatContact>> contactsCallback) { Server This is a similar method that allows us to get a user based on a user ID instead of a phone. If we get a chat message that was sent by a specific user we will need to know about that user. This method lets us fetch the meta data related to that user.
  • 64. } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void fetchChatList( OnComplete<List<ChatContact>> contactsCallback) { fetchContacts(cl -> { List<ChatContact> response = new ArrayList<>(); for(ChatContact c : cl) { if(c.lastActivityTime.get() != null) { response.add(c); } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). Server The chats we have open with users can be extracted from the list of contacts. Since every contact had its own chat thread.
  • 65. } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void fetchChatList( OnComplete<List<ChatContact>> contactsCallback) { fetchContacts(cl -> { List<ChatContact> response = new ArrayList<>(); for(ChatContact c : cl) { if(c.lastActivityTime.get() != null) { response.add(c); } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). Server So to fetch the chats we see in the main form of the whatsapp UI we need to first fetch the contacts as they might not have been loaded yet.
  • 66. } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void fetchChatList( OnComplete<List<ChatContact>> contactsCallback) { fetchContacts(cl -> { List<ChatContact> response = new ArrayList<>(); for(ChatContact c : cl) { if(c.lastActivityTime.get() != null) { response.add(c); } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). Server We loop over the contacts and if we had activity with that contact we add him to the list in the response
  • 67. } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void fetchChatList( OnComplete<List<ChatContact>> contactsCallback) { fetchContacts(cl -> { List<ChatContact> response = new ArrayList<>(); for(ChatContact c : cl) { if(c.lastActivityTime.get() != null) { response.add(c); } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). Server But before we finish we need to sort the responses based on activity time. The sort method is builtin to the Java collections API. It accepts a comparator which we represented here as a lambda expression
  • 68. } resultCallback.completed((ChatContact)l.get(0)); }, ChatContact.class); } public static void fetchChatList( OnComplete<List<ChatContact>> contactsCallback) { fetchContacts(cl -> { List<ChatContact> response = new ArrayList<>(); for(ChatContact c : cl) { if(c.lastActivityTime.get() != null) { response.add(c); } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). Server The comparator compares two objects in the list to one another. It returns a value smaller than 0 to indicate the first value is smaller. zero to indicate the values are identical and more than 0 to indicate the second value is larger. The simple solution is subtracting the time values to get a valid comparison result.
  • 69. } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). body(messageId).fetchAsString(c -> {}); } public static void updatePushKey(String key) { if(user() != null) { get("user/updatePushKey"). queryParam("id", user().id.get()). queryParam("key", key).fetchAsString(c -> {}); } } } Server We saw the ack call earlier. This stands for acknowledgement. We effectively acknowledge that a message was received. If this doesn’t go out the server doesn’t know if a message reached its destination
  • 70. } } Collections.sort(response, (ChatContact o1, ChatContact o2) -> (int)(o1.lastActivityTime.get().getTime() - o2.lastActivityTime.get().getTime())); contactsCallback.completed(response); }); } public static void ackMessage(String messageId) { post("user/ackMessage"). body(messageId).fetchAsString(c -> {}); } public static void updatePushKey(String key) { if(user() != null) { get("user/updatePushKey"). queryParam("id", user().id.get()). queryParam("key", key).fetchAsString(c -> {}); } } } Server Finally we need this method for push notification. It sends the push key of the device to the server so the server will be able to send push messages to the devices.