Unleash Your Potential - Namagunga Girls Coding Club
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.