SlideShare a Scribd company logo
1 of 70
Download to read offline
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

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

Hi, I need some one to help me with Design a client-server Chat so.pdf
Hi, I need some one to help me with Design a client-server Chat so.pdfHi, I need some one to help me with Design a client-server Chat so.pdf
Hi, I need some one to help me with Design a client-server Chat so.pdffashiongallery1
 
I really need help on this question.Create a program that allows t.pdf
I really need help on this question.Create a program that allows t.pdfI really need help on this question.Create a program that allows t.pdf
I really need help on this question.Create a program that allows t.pdfamitbagga0808
 
Speed up your Web applications with HTML5 WebSockets
Speed up your Web applications with HTML5 WebSocketsSpeed up your Web applications with HTML5 WebSockets
Speed up your Web applications with HTML5 WebSocketsYakov Fain
 
Use Windows Azure Service Bus, BizTalk Services, Mobile Services, and BizTalk...
Use Windows Azure Service Bus, BizTalk Services, Mobile Services, and BizTalk...Use Windows Azure Service Bus, BizTalk Services, Mobile Services, and BizTalk...
Use Windows Azure Service Bus, BizTalk Services, Mobile Services, and BizTalk...BizTalk360
 
Distributed Objects and JAVA
Distributed Objects and JAVADistributed Objects and JAVA
Distributed Objects and JAVAelliando dias
 
Creating a Facebook Clone - Part XXVII - Transcript.pdf
Creating a Facebook Clone - Part XXVII - Transcript.pdfCreating a Facebook Clone - Part XXVII - Transcript.pdf
Creating a Facebook Clone - Part XXVII - Transcript.pdfShaiAlmog1
 
Creating a Whatsapp Clone - Part XIV - Transcript.pdf
Creating a Whatsapp Clone - Part XIV - Transcript.pdfCreating a Whatsapp Clone - Part XIV - Transcript.pdf
Creating a Whatsapp Clone - Part XIV - Transcript.pdfShaiAlmog1
 
The Full Power of ASP.NET Web API
The Full Power of ASP.NET Web APIThe Full Power of ASP.NET Web API
The Full Power of ASP.NET Web APIEyal Vardi
 
Creating a Whatsapp Clone - Part XV - Transcript.pdf
Creating a Whatsapp Clone - Part XV - Transcript.pdfCreating a Whatsapp Clone - Part XV - Transcript.pdf
Creating a Whatsapp Clone - Part XV - Transcript.pdfShaiAlmog1
 
Creating an Uber Clone - Part XIII - Transcript.pdf
Creating an Uber Clone - Part XIII - Transcript.pdfCreating an Uber Clone - Part XIII - Transcript.pdf
Creating an Uber Clone - Part XIII - Transcript.pdfShaiAlmog1
 
Node.js System: The Approach
Node.js System: The ApproachNode.js System: The Approach
Node.js System: The ApproachHaci Murat Yaman
 
4th semester project report
4th semester project report4th semester project report
4th semester project reportAkash Rajguru
 
Integrating Wicket with Java EE 6
Integrating Wicket with Java EE 6Integrating Wicket with Java EE 6
Integrating Wicket with Java EE 6Michael Plöd
 
Laporan multi client
Laporan multi clientLaporan multi client
Laporan multi clientichsanbarokah
 
Local SQLite Database with Node for beginners
Local SQLite Database with Node for beginnersLocal SQLite Database with Node for beginners
Local SQLite Database with Node for beginnersLaurence Svekis ✔
 

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

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

More from ShaiAlmog1

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

More from ShaiAlmog1 (20)

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

Recently uploaded

Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Patryk Bandurski
 
Bun (KitWorks Team Study 노별마루 발표 2024.4.22)
Bun (KitWorks Team Study 노별마루 발표 2024.4.22)Bun (KitWorks Team Study 노별마루 발표 2024.4.22)
Bun (KitWorks Team Study 노별마루 발표 2024.4.22)Wonjun Hwang
 
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024BookNet Canada
 
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks..."LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...Fwdays
 
Snow Chain-Integrated Tire for a Safe Drive on Winter Roads
Snow Chain-Integrated Tire for a Safe Drive on Winter RoadsSnow Chain-Integrated Tire for a Safe Drive on Winter Roads
Snow Chain-Integrated Tire for a Safe Drive on Winter RoadsHyundai Motor Group
 
Build your next Gen AI Breakthrough - April 2024
Build your next Gen AI Breakthrough - April 2024Build your next Gen AI Breakthrough - April 2024
Build your next Gen AI Breakthrough - April 2024Neo4j
 
"Federated learning: out of reach no matter how close",Oleksandr Lapshyn
"Federated learning: out of reach no matter how close",Oleksandr Lapshyn"Federated learning: out of reach no matter how close",Oleksandr Lapshyn
"Federated learning: out of reach no matter how close",Oleksandr LapshynFwdays
 
Unlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsUnlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsPrecisely
 
Science&tech:THE INFORMATION AGE STS.pdf
Science&tech:THE INFORMATION AGE STS.pdfScience&tech:THE INFORMATION AGE STS.pdf
Science&tech:THE INFORMATION AGE STS.pdfjimielynbastida
 
Human Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsHuman Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsMark Billinghurst
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 3652toLead Limited
 
Understanding the Laravel MVC Architecture
Understanding the Laravel MVC ArchitectureUnderstanding the Laravel MVC Architecture
Understanding the Laravel MVC ArchitecturePixlogix Infotech
 
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024BookNet Canada
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Alan Dix
 
Artificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning eraArtificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning eraDeakin University
 
Install Stable Diffusion in windows machine
Install Stable Diffusion in windows machineInstall Stable Diffusion in windows machine
Install Stable Diffusion in windows machinePadma Pradeep
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksSoftradix Technologies
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationSlibray Presentation
 

Recently uploaded (20)

Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
 
Bun (KitWorks Team Study 노별마루 발표 2024.4.22)
Bun (KitWorks Team Study 노별마루 발표 2024.4.22)Bun (KitWorks Team Study 노별마루 발표 2024.4.22)
Bun (KitWorks Team Study 노별마루 발표 2024.4.22)
 
Hot Sexy call girls in Panjabi Bagh 🔝 9953056974 🔝 Delhi escort Service
Hot Sexy call girls in Panjabi Bagh 🔝 9953056974 🔝 Delhi escort ServiceHot Sexy call girls in Panjabi Bagh 🔝 9953056974 🔝 Delhi escort Service
Hot Sexy call girls in Panjabi Bagh 🔝 9953056974 🔝 Delhi escort Service
 
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
 
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks..."LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
 
Snow Chain-Integrated Tire for a Safe Drive on Winter Roads
Snow Chain-Integrated Tire for a Safe Drive on Winter RoadsSnow Chain-Integrated Tire for a Safe Drive on Winter Roads
Snow Chain-Integrated Tire for a Safe Drive on Winter Roads
 
Build your next Gen AI Breakthrough - April 2024
Build your next Gen AI Breakthrough - April 2024Build your next Gen AI Breakthrough - April 2024
Build your next Gen AI Breakthrough - April 2024
 
"Federated learning: out of reach no matter how close",Oleksandr Lapshyn
"Federated learning: out of reach no matter how close",Oleksandr Lapshyn"Federated learning: out of reach no matter how close",Oleksandr Lapshyn
"Federated learning: out of reach no matter how close",Oleksandr Lapshyn
 
Unlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsUnlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power Systems
 
Science&tech:THE INFORMATION AGE STS.pdf
Science&tech:THE INFORMATION AGE STS.pdfScience&tech:THE INFORMATION AGE STS.pdf
Science&tech:THE INFORMATION AGE STS.pdf
 
Human Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsHuman Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR Systems
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
 
Understanding the Laravel MVC Architecture
Understanding the Laravel MVC ArchitectureUnderstanding the Laravel MVC Architecture
Understanding the Laravel MVC Architecture
 
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
 
Artificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning eraArtificial intelligence in the post-deep learning era
Artificial intelligence in the post-deep learning era
 
Install Stable Diffusion in windows machine
Install Stable Diffusion in windows machineInstall Stable Diffusion in windows machine
Install Stable Diffusion in windows machine
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other Frameworks
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck Presentation
 
The transition to renewables in India.pdf
The transition to renewables in India.pdfThe transition to renewables in India.pdf
The transition to renewables in India.pdf
 

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.