KEMBAR78
Creating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a WhatsApp Clone - Part IV
There is still one other class within the model package once we finish that we’ll go to the main class
public class ChatMessage implements PropertyBusinessObject {
public final Property<String, ChatMessage> id = new Property<>("id");
public final Property<String, ChatMessage> authorId =
new Property<>("authorId");
public final Property<String, ChatMessage> authorPhone =
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
ChatMessage
The chat message class is another property business object but one that’s even simpler than the last one
public class ChatMessage implements PropertyBusinessObject {
public final Property<String, ChatMessage> id = new Property<>("id");
public final Property<String, ChatMessage> authorId =
new Property<>("authorId");
public final Property<String, ChatMessage> authorPhone =
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
ChatMessage
This is the author of the message. Since we use ID’s and not phones this might become an issue so we need to keep track of both.
public class ChatMessage implements PropertyBusinessObject {
public final Property<String, ChatMessage> id = new Property<>("id");
public final Property<String, ChatMessage> authorId =
new Property<>("authorId");
public final Property<String, ChatMessage> authorPhone =
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
ChatMessage
Sent to can be important as this message might be been sent to a group and not directly to our current user
public class ChatMessage implements PropertyBusinessObject {
public final Property<String, ChatMessage> id = new Property<>("id");
public final Property<String, ChatMessage> authorId =
new Property<>("authorId");
public final Property<String, ChatMessage> authorPhone =
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
ChatMessage
The timestamp of the message and the actual text of the message are the main payload.
public class ChatMessage implements PropertyBusinessObject {
public final Property<String, ChatMessage> id = new Property<>("id");
public final Property<String, ChatMessage> authorId =
new Property<>("authorId");
public final Property<String, ChatMessage> authorPhone =
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
ChatMessage
Attachments aren’t fully implemented. The general idea is a URL of the attachment mapping to a mime type so we can represent it in the UI as image/video, audio or
document.
public class ChatMessage implements PropertyBusinessObject {
public final Property<String, ChatMessage> id = new Property<>("id");
public final Property<String, ChatMessage> authorId =
new Property<>("authorId");
public final Property<String, ChatMessage> authorPhone =
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
ChatMessage
The list of people who viewed the message which can be more than 1 for a message sent to a group.
new Property<>("authorPhone");
public final Property<String, ChatMessage> sentTo =
new Property<>("sentTo");
public final Property<Date, ChatMessage> time =
new Property<>("time", Date.class);
public final Property<String, ChatMessage> body =
new Property<>("body");
public final MapProperty<String, String, ChatMessage> attachments =
new MapProperty<>("media", String.class, String.class);
public final SetProperty<String, ChatMessage> viewedBy =
new SetProperty<>("viewedBy", String.class);
public final BooleanProperty<ChatMessage> typing =
new BooleanProperty<>("typing");
private final PropertyIndex idx = new PropertyIndex(this,
"ChatMessage", id, authorId, authorPhone, sentTo, time,
body, attachments, viewedBy, typing);
@Override
public PropertyIndex getPropertyIndex() {
return idx;
}
}
ChatMessage
The typing message is a special type of message that we don’t currently send. But it can be sent pretty easily and just update the UI that the user is typing to this chat.

This is the final property in this class which is relatively simple
public class WhatsAppClone implements PushCallback {
private Form current;
private Resources theme;
public void init(Object context) {
// use two network threads instead of one
updateNetworkThreadCount(2);
theme = UIManager.initFirstTheme("/theme");
// Enable Toolbar on all Forms by default
Toolbar.setGlobalToolbar(true);
// Pro only feature
Log.bindCrashProtection(true);
Server.init();
addNetworkErrorListener(err -> {
WhatsAppClone
With that long de-tour out of the way lets go back to the main class. As you can see left most of the code in-tact and kept it as the default.

The first piece of code you will see that isn't a part of the default code is this line to initialize the server and load the saved data
public class WhatsAppClone implements PushCallback {
private Form current;
private Resources theme;
public void init(Object context) {
// use two network threads instead of one
updateNetworkThreadCount(2);
theme = UIManager.initFirstTheme("/theme");
// Enable Toolbar on all Forms by default
Toolbar.setGlobalToolbar(true);
// Pro only feature
Log.bindCrashProtection(true);
Server.init();
addNetworkErrorListener(err -> {
WhatsAppClone
There is also the push interface which we need to implement to receive push callbacks
// Pro only feature
Log.bindCrashProtection(true);
Server.init();
addNetworkErrorListener(err -> {
// prevent the event from propagating
err.consume();
if(err.getError() != null) {
Log.e(err.getError());
}
Log.sendLogAsync();
Dialog.show("Connection Error",
"There was a networking error in the connection to " +
err.getConnectionRequest().getUrl(), "OK", null);
});
}
@Override
public void push(String value) {
}
@Override
WhatsAppClone
That leads us directly to the first method from that interface. We don’t need to implement this method since we use push only as a visual medium and rely on websockets
to carry the actual data. Push is inherently unreliable and might perform badly. It places limitations on the type of data you can send. We have more control over
websockets. We use push only when the app is minimized.
err.getConnectionRequest().getUrl(), "OK", null);
});
}
@Override
public void push(String value) {
}
@Override
public void registeredForPush(String deviceId) {
Server.updatePushKey(Push.getPushKey());
}
@Override
public void pushRegistrationError(String error, int errorCode) {
}
static class SMSVerifyImpl extends SMSVerification {
@Override
public void sendSMSCode(String phone) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
Server.signup(new ChatContact().phone.set(phone),
c -> d.dispose());
}
WhatsAppClone
The one method we need to implement from the push interface is the registered for push callback. When this callback is invoked we need to send the push key to the
server. This is important: notice that the push key isn’t the device ID. They are different values, don’t confuse them!
@Override
public void pushRegistrationError(String error, int errorCode) {
}
static class SMSVerifyImpl extends SMSVerification {
@Override
public void sendSMSCode(String phone) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
Server.signup(new ChatContact().phone.set(phone),
c -> d.dispose());
}
@Override
public void verifySmsCode(String code,
SuccessCallback<Boolean> s) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
boolean result = Server.verify(code);
d.dispose();
s.onSucess(result);
}
}
private void bindMessageListener() {
WhatsAppClone
The SMSVerification class is an abstract class from the SMS verification cn1lib. It lets us move some of the functionality of that library into the server.
@Override
public void pushRegistrationError(String error, int errorCode) {
}
static class SMSVerifyImpl extends SMSVerification {
@Override
public void sendSMSCode(String phone) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
Server.signup(new ChatContact().phone.set(phone),
c -> d.dispose());
}
@Override
public void verifySmsCode(String code,
SuccessCallback<Boolean> s) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
boolean result = Server.verify(code);
d.dispose();
s.onSucess(result);
}
}
private void bindMessageListener() {
WhatsAppClone
The first method is the sendSMSCode method. It sends an SMS message to the given phone number. On the server it invokes the signup call which triggers an SMS to
that phone number. This callback is invoked as part of the signup process.
@Override
public void pushRegistrationError(String error, int errorCode) {
}
static class SMSVerifyImpl extends SMSVerification {
@Override
public void sendSMSCode(String phone) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
Server.signup(new ChatContact().phone.set(phone),
c -> d.dispose());
}
@Override
public void verifySmsCode(String code,
SuccessCallback<Boolean> s) {
Dialog d = new InfiniteProgress().showInfiniteBlocking();
boolean result = Server.verify(code);
d.dispose();
s.onSucess(result);
}
}
private void bindMessageListener() {
WhatsAppClone
When the user types in or the system intercepts a phone number this callback is invoked, it send the verification string to the server side and returns the result based on
that
Dialog d = new InfiniteProgress().showInfiniteBlocking();
boolean result = Server.verify(code);
d.dispose();
s.onSucess(result);
}
}
private void bindMessageListener() {
Server.bindMessageListener(new ServerMessages() {
@Override
public void connected() {
}
@Override
public void disconnected() {
}
@Override
public void messageReceived(ChatMessage m) {
Form f = getCurrentForm();
if(f instanceof ChatForm) {
ChatForm cf = (ChatForm)f;
if(cf.getContact().id.equals(m.authorId.get())) {
cf.addMessageToUI(m);
WhatsAppClone
The message listener allows us to track messages from the server such as connect, incoming messages etc. A lot of this isn’t implemented as we don’t need it right now
but it could be useful for the UI as it evolves.
@Override
public void disconnected() {
}
@Override
public void messageReceived(ChatMessage m) {
Form f = getCurrentForm();
if(f instanceof ChatForm) {
ChatForm cf = (ChatForm)f;
if(cf.getContact().id.equals(m.authorId.get())) {
cf.addMessageToUI(m);
}
}
MainForm.getInstance().refreshChatsContainer();
}
@Override
public void userTyping(String contactId) {
}
@Override
public void messageViewed(String msgId, List<String> userIds) {
}
WhatsAppClone
One thing we do implement here is the message received API. There isn’t much going on in this method though
@Override
public void disconnected() {
}
@Override
public void messageReceived(ChatMessage m) {
Form f = getCurrentForm();
if(f instanceof ChatForm) {
ChatForm cf = (ChatForm)f;
if(cf.getContact().id.equals(m.authorId.get())) {
cf.addMessageToUI(m);
}
}
MainForm.getInstance().refreshChatsContainer();
}
@Override
public void userTyping(String contactId) {
}
@Override
public void messageViewed(String msgId, List<String> userIds) {
}
WhatsAppClone
If the current form is the chat form
@Override
public void disconnected() {
}
@Override
public void messageReceived(ChatMessage m) {
Form f = getCurrentForm();
if(f instanceof ChatForm) {
ChatForm cf = (ChatForm)f;
if(cf.getContact().id.equals(m.authorId.get())) {
cf.addMessageToUI(m);
}
}
MainForm.getInstance().refreshChatsContainer();
}
@Override
public void userTyping(String contactId) {
}
@Override
public void messageViewed(String msgId, List<String> userIds) {
}
WhatsAppClone
Then we need to check if we are currently in the chat form with the sender of the incoming message. Assuming this is the case we can add this message to the UI
@Override
public void disconnected() {
}
@Override
public void messageReceived(ChatMessage m) {
Form f = getCurrentForm();
if(f instanceof ChatForm) {
ChatForm cf = (ChatForm)f;
if(cf.getContact().id.equals(m.authorId.get())) {
cf.addMessageToUI(m);
}
}
MainForm.getInstance().refreshChatsContainer();
}
@Override
public void userTyping(String contactId) {
}
@Override
public void messageViewed(String msgId, List<String> userIds) {
}
WhatsAppClone
Regardless we need to refresh the main UI of the chat list container. Since the order to the contacts will change.
}
@Override
public void userTyping(String contactId) {
}
@Override
public void messageViewed(String msgId, List<String> userIds) {
}
});
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
WhatsAppClone
Next we have the start() lifecycle method
}
@Override
public void userTyping(String contactId) {
}
@Override
public void messageViewed(String msgId, List<String> userIds) {
}
});
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
WhatsAppClone
You will notice that we invoke bindMessageListener() even when we restore a running app. As you might recall we close the websocket connection when the app is
minimized. This effectively restores that connection when the app is restored back to normal
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
This call happens when the app is launched in a “cold start”
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
If the phone number isn’t set this is the first activation and we need to setup a new user
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
The activation form API builds the data and UI using a builder pattern where every method adds to the resulting form.
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
First we allocate the activation form with the title “Signup”
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
We then determine that we want a 6 digit activation code instead of the default 4 digit code
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
We finally show the activation UI. This accepts two arguments
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
The second argument is the SMS verification subclass we discussed earlier. It sends the sms details to the server which issues an SMS. It then performs the verification
on the server which is more secure than client side verification.
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
That first argument is a callback that’s invoked when the activation is completed. It’s invoked with the phone number in the result. Here we store the new phone number
to the preferences then show the main form UI.
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
If the user was already registered we show the main form directly.
}
public void start() {
if(current != null){
current.show();
bindMessageListener();
return;
}
String phoneNumber = Preferences.get("PhoneNumber", null);
if(phoneNumber == null) {
ActivationForm.create("Signup").
codeDigits(6).
show(s -> {
Log.p("SMS Activation returned " + s);
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
WhatsAppClone
We discussed the bind message before so the last piece is the registerPush call. This is an essential part of the push notification support
Preferences.set("PhoneNumber", s);
new MainForm().show();
}, new SMSVerifyImpl());
} else {
new MainForm().show();
}
bindMessageListener();
callSerially(() -> registerPush());
}
public void stop() {
Server.closeWebsocketConnection();
current = getCurrentForm();
if(current instanceof Dialog) {
((Dialog)current).dispose();
current = getCurrentForm();
}
}
public void destroy() {
}
}
WhatsAppClone
Finally we added close websocket code to the stop() method. That implements the logic of stopping the websocket connection when the app is minimized.

Creating a Whatsapp Clone - Part IV - Transcript.pdf

  • 1.
    Creating a WhatsAppClone - Part IV There is still one other class within the model package once we finish that we’ll go to the main class
  • 2.
    public class ChatMessageimplements PropertyBusinessObject { public final Property<String, ChatMessage> id = new Property<>("id"); public final Property<String, ChatMessage> authorId = new Property<>("authorId"); public final Property<String, ChatMessage> authorPhone = new Property<>("authorPhone"); public final Property<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, ChatMessage The chat message class is another property business object but one that’s even simpler than the last one
  • 3.
    public class ChatMessageimplements PropertyBusinessObject { public final Property<String, ChatMessage> id = new Property<>("id"); public final Property<String, ChatMessage> authorId = new Property<>("authorId"); public final Property<String, ChatMessage> authorPhone = new Property<>("authorPhone"); public final Property<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, ChatMessage This is the author of the message. Since we use ID’s and not phones this might become an issue so we need to keep track of both.
  • 4.
    public class ChatMessageimplements PropertyBusinessObject { public final Property<String, ChatMessage> id = new Property<>("id"); public final Property<String, ChatMessage> authorId = new Property<>("authorId"); public final Property<String, ChatMessage> authorPhone = new Property<>("authorPhone"); public final Property<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, ChatMessage Sent to can be important as this message might be been sent to a group and not directly to our current user
  • 5.
    public class ChatMessageimplements PropertyBusinessObject { public final Property<String, ChatMessage> id = new Property<>("id"); public final Property<String, ChatMessage> authorId = new Property<>("authorId"); public final Property<String, ChatMessage> authorPhone = new Property<>("authorPhone"); public final Property<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, ChatMessage The timestamp of the message and the actual text of the message are the main payload.
  • 6.
    public class ChatMessageimplements PropertyBusinessObject { public final Property<String, ChatMessage> id = new Property<>("id"); public final Property<String, ChatMessage> authorId = new Property<>("authorId"); public final Property<String, ChatMessage> authorPhone = new Property<>("authorPhone"); public final Property<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, ChatMessage Attachments aren’t fully implemented. The general idea is a URL of the attachment mapping to a mime type so we can represent it in the UI as image/video, audio or document.
  • 7.
    public class ChatMessageimplements PropertyBusinessObject { public final Property<String, ChatMessage> id = new Property<>("id"); public final Property<String, ChatMessage> authorId = new Property<>("authorId"); public final Property<String, ChatMessage> authorPhone = new Property<>("authorPhone"); public final Property<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, ChatMessage The list of people who viewed the message which can be more than 1 for a message sent to a group.
  • 8.
    new Property<>("authorPhone"); public finalProperty<String, ChatMessage> sentTo = new Property<>("sentTo"); public final Property<Date, ChatMessage> time = new Property<>("time", Date.class); public final Property<String, ChatMessage> body = new Property<>("body"); public final MapProperty<String, String, ChatMessage> attachments = new MapProperty<>("media", String.class, String.class); public final SetProperty<String, ChatMessage> viewedBy = new SetProperty<>("viewedBy", String.class); public final BooleanProperty<ChatMessage> typing = new BooleanProperty<>("typing"); private final PropertyIndex idx = new PropertyIndex(this, "ChatMessage", id, authorId, authorPhone, sentTo, time, body, attachments, viewedBy, typing); @Override public PropertyIndex getPropertyIndex() { return idx; } } ChatMessage The typing message is a special type of message that we don’t currently send. But it can be sent pretty easily and just update the UI that the user is typing to this chat. This is the final property in this class which is relatively simple
  • 9.
    public class WhatsAppCloneimplements PushCallback { private Form current; private Resources theme; public void init(Object context) { // use two network threads instead of one updateNetworkThreadCount(2); theme = UIManager.initFirstTheme("/theme"); // Enable Toolbar on all Forms by default Toolbar.setGlobalToolbar(true); // Pro only feature Log.bindCrashProtection(true); Server.init(); addNetworkErrorListener(err -> { WhatsAppClone With that long de-tour out of the way lets go back to the main class. As you can see left most of the code in-tact and kept it as the default. The first piece of code you will see that isn't a part of the default code is this line to initialize the server and load the saved data
  • 10.
    public class WhatsAppCloneimplements PushCallback { private Form current; private Resources theme; public void init(Object context) { // use two network threads instead of one updateNetworkThreadCount(2); theme = UIManager.initFirstTheme("/theme"); // Enable Toolbar on all Forms by default Toolbar.setGlobalToolbar(true); // Pro only feature Log.bindCrashProtection(true); Server.init(); addNetworkErrorListener(err -> { WhatsAppClone There is also the push interface which we need to implement to receive push callbacks
  • 11.
    // Pro onlyfeature Log.bindCrashProtection(true); Server.init(); addNetworkErrorListener(err -> { // prevent the event from propagating err.consume(); if(err.getError() != null) { Log.e(err.getError()); } Log.sendLogAsync(); Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null); }); } @Override public void push(String value) { } @Override WhatsAppClone That leads us directly to the first method from that interface. We don’t need to implement this method since we use push only as a visual medium and rely on websockets to carry the actual data. Push is inherently unreliable and might perform badly. It places limitations on the type of data you can send. We have more control over websockets. We use push only when the app is minimized.
  • 12.
    err.getConnectionRequest().getUrl(), "OK", null); }); } @Override publicvoid push(String value) { } @Override public void registeredForPush(String deviceId) { Server.updatePushKey(Push.getPushKey()); } @Override public void pushRegistrationError(String error, int errorCode) { } static class SMSVerifyImpl extends SMSVerification { @Override public void sendSMSCode(String phone) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); Server.signup(new ChatContact().phone.set(phone), c -> d.dispose()); } WhatsAppClone The one method we need to implement from the push interface is the registered for push callback. When this callback is invoked we need to send the push key to the server. This is important: notice that the push key isn’t the device ID. They are different values, don’t confuse them!
  • 13.
    @Override public void pushRegistrationError(Stringerror, int errorCode) { } static class SMSVerifyImpl extends SMSVerification { @Override public void sendSMSCode(String phone) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); Server.signup(new ChatContact().phone.set(phone), c -> d.dispose()); } @Override public void verifySmsCode(String code, SuccessCallback<Boolean> s) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); boolean result = Server.verify(code); d.dispose(); s.onSucess(result); } } private void bindMessageListener() { WhatsAppClone The SMSVerification class is an abstract class from the SMS verification cn1lib. It lets us move some of the functionality of that library into the server.
  • 14.
    @Override public void pushRegistrationError(Stringerror, int errorCode) { } static class SMSVerifyImpl extends SMSVerification { @Override public void sendSMSCode(String phone) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); Server.signup(new ChatContact().phone.set(phone), c -> d.dispose()); } @Override public void verifySmsCode(String code, SuccessCallback<Boolean> s) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); boolean result = Server.verify(code); d.dispose(); s.onSucess(result); } } private void bindMessageListener() { WhatsAppClone The first method is the sendSMSCode method. It sends an SMS message to the given phone number. On the server it invokes the signup call which triggers an SMS to that phone number. This callback is invoked as part of the signup process.
  • 15.
    @Override public void pushRegistrationError(Stringerror, int errorCode) { } static class SMSVerifyImpl extends SMSVerification { @Override public void sendSMSCode(String phone) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); Server.signup(new ChatContact().phone.set(phone), c -> d.dispose()); } @Override public void verifySmsCode(String code, SuccessCallback<Boolean> s) { Dialog d = new InfiniteProgress().showInfiniteBlocking(); boolean result = Server.verify(code); d.dispose(); s.onSucess(result); } } private void bindMessageListener() { WhatsAppClone When the user types in or the system intercepts a phone number this callback is invoked, it send the verification string to the server side and returns the result based on that
  • 16.
    Dialog d =new InfiniteProgress().showInfiniteBlocking(); boolean result = Server.verify(code); d.dispose(); s.onSucess(result); } } private void bindMessageListener() { Server.bindMessageListener(new ServerMessages() { @Override public void connected() { } @Override public void disconnected() { } @Override public void messageReceived(ChatMessage m) { Form f = getCurrentForm(); if(f instanceof ChatForm) { ChatForm cf = (ChatForm)f; if(cf.getContact().id.equals(m.authorId.get())) { cf.addMessageToUI(m); WhatsAppClone The message listener allows us to track messages from the server such as connect, incoming messages etc. A lot of this isn’t implemented as we don’t need it right now but it could be useful for the UI as it evolves.
  • 17.
    @Override public void disconnected(){ } @Override public void messageReceived(ChatMessage m) { Form f = getCurrentForm(); if(f instanceof ChatForm) { ChatForm cf = (ChatForm)f; if(cf.getContact().id.equals(m.authorId.get())) { cf.addMessageToUI(m); } } MainForm.getInstance().refreshChatsContainer(); } @Override public void userTyping(String contactId) { } @Override public void messageViewed(String msgId, List<String> userIds) { } WhatsAppClone One thing we do implement here is the message received API. There isn’t much going on in this method though
  • 18.
    @Override public void disconnected(){ } @Override public void messageReceived(ChatMessage m) { Form f = getCurrentForm(); if(f instanceof ChatForm) { ChatForm cf = (ChatForm)f; if(cf.getContact().id.equals(m.authorId.get())) { cf.addMessageToUI(m); } } MainForm.getInstance().refreshChatsContainer(); } @Override public void userTyping(String contactId) { } @Override public void messageViewed(String msgId, List<String> userIds) { } WhatsAppClone If the current form is the chat form
  • 19.
    @Override public void disconnected(){ } @Override public void messageReceived(ChatMessage m) { Form f = getCurrentForm(); if(f instanceof ChatForm) { ChatForm cf = (ChatForm)f; if(cf.getContact().id.equals(m.authorId.get())) { cf.addMessageToUI(m); } } MainForm.getInstance().refreshChatsContainer(); } @Override public void userTyping(String contactId) { } @Override public void messageViewed(String msgId, List<String> userIds) { } WhatsAppClone Then we need to check if we are currently in the chat form with the sender of the incoming message. Assuming this is the case we can add this message to the UI
  • 20.
    @Override public void disconnected(){ } @Override public void messageReceived(ChatMessage m) { Form f = getCurrentForm(); if(f instanceof ChatForm) { ChatForm cf = (ChatForm)f; if(cf.getContact().id.equals(m.authorId.get())) { cf.addMessageToUI(m); } } MainForm.getInstance().refreshChatsContainer(); } @Override public void userTyping(String contactId) { } @Override public void messageViewed(String msgId, List<String> userIds) { } WhatsAppClone Regardless we need to refresh the main UI of the chat list container. Since the order to the contacts will change.
  • 21.
    } @Override public void userTyping(StringcontactId) { } @Override public void messageViewed(String msgId, List<String> userIds) { } }); } public void start() { if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); WhatsAppClone Next we have the start() lifecycle method
  • 22.
    } @Override public void userTyping(StringcontactId) { } @Override public void messageViewed(String msgId, List<String> userIds) { } }); } public void start() { if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); WhatsAppClone You will notice that we invoke bindMessageListener() even when we restore a running app. As you might recall we close the websocket connection when the app is minimized. This effectively restores that connection when the app is restored back to normal
  • 23.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone This call happens when the app is launched in a “cold start”
  • 24.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone If the phone number isn’t set this is the first activation and we need to setup a new user
  • 25.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone The activation form API builds the data and UI using a builder pattern where every method adds to the resulting form.
  • 26.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone First we allocate the activation form with the title “Signup”
  • 27.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone We then determine that we want a 6 digit activation code instead of the default 4 digit code
  • 28.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone We finally show the activation UI. This accepts two arguments
  • 29.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone The second argument is the SMS verification subclass we discussed earlier. It sends the sms details to the server which issues an SMS. It then performs the verification on the server which is more secure than client side verification.
  • 30.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone That first argument is a callback that’s invoked when the activation is completed. It’s invoked with the phone number in the result. Here we store the new phone number to the preferences then show the main form UI.
  • 31.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone If the user was already registered we show the main form directly.
  • 32.
    } public void start(){ if(current != null){ current.show(); bindMessageListener(); return; } String phoneNumber = Preferences.get("PhoneNumber", null); if(phoneNumber == null) { ActivationForm.create("Signup"). codeDigits(6). show(s -> { Log.p("SMS Activation returned " + s); Preferences.set("PhoneNumber", s); new MainForm().show(); }, new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } WhatsAppClone We discussed the bind message before so the last piece is the registerPush call. This is an essential part of the push notification support
  • 33.
    Preferences.set("PhoneNumber", s); new MainForm().show(); },new SMSVerifyImpl()); } else { new MainForm().show(); } bindMessageListener(); callSerially(() -> registerPush()); } public void stop() { Server.closeWebsocketConnection(); current = getCurrentForm(); if(current instanceof Dialog) { ((Dialog)current).dispose(); current = getCurrentForm(); } } public void destroy() { } } WhatsAppClone Finally we added close websocket code to the stop() method. That implements the logic of stopping the websocket connection when the app is minimized.