Software Development

Building A Chat App With Codename One Part 6 – Codename One

This will be the last installment of this tutorial which was pretty complete in the previous section already. We might have additional installments mostly for covering enhancements such as “invite a friend” and other similar capabilites but this is probably as good a place as any to finish the app and let you try it live.

Native Push

Up until now we used PubNub to implement the push functionality which is excellent especially if you opt for the paid option which can also persist messages and offers quite a few additional perks for an app such as this. However, when the app isn’t running PubNub can’t push anything and in that case we need an OS native push to send the message.

The obvious question would be “why not use OS native push for everything?”.

Its possible to do so but OS native push isn’t as flexible, portable, fast or reliable as PubNub. Its painful to work with and the user can intentionally or inadvertently break it by disagreeing to an OS prompt etc…​ As you will see from the rest of the tutorial where we fallback to native OS push when PubNub can’t reach our target, its no panacea.

The PushCallback Interface

We start by implementing the PushCallback interface in our main class, this must be the actual main class or push won’t work:

public class SocialChat implements PushCallback {

Then we need to implement the following methods:

@Override
public void push(String value) {
    // its a JSON message, otherwise its a notice to the user
    if(value.startsWith("{") || value.startsWith("[")) {
        try {
            JSONObject obj = new JSONObject(value);

            // this is still early since we probably didn't login yet so add the messages to the list of pending messages
            java.util.List<Message> pendingMessages = (java.util.List<Message>)Storage.getInstance().readObject("pendingMessages");
            if(pendingMessages == null) {
                pendingMessages = new ArrayList<>();
            }
            Message m = new Message(obj);
            pendingMessages.add(m);
            Storage.getInstance().writeObject("pendingMessages", pendingMessages);
            addMessage(m);
        } catch(JSONException err) {
            err.printStackTrace();
        }
    }
}

@Override
public void registeredForPush(String deviceId) {
}

@Override
public void pushRegistrationError(String error, int errorCode) {
}

You will notice the following things here:

  • We don’t really need anything in the registeredForPush or pushRegistrationError. Since we use PubNub even if push fails the app will still work fine. registeredForPush is normally used to get the push key (which isn’t the argument passed to that method its the Push.getPushKey()) and send it to your servers so you can trigger a push to this device. Since here we don’t have a real server we have no use for that method.
  • The push method does the heavy lifting of handling push messages. It can be called anytime and accepts the push callbacks. It receives both visible and hidden push messages and decides what to do with them based on their content.
  • We don’t show the message during the push callback. It will be invoked before the user had time to login and so we want to just store the Message objects and have them processed later. We still add them to the general store in case the user decides to kill the app before actually logging in

New Constants & Registration

We need to add the following variables to the class to continue, I masked and changed the values. We’ll go over them one by one:

private static final String PUSH_TOKEN = "********-****-****-****-*************";
private static final String GCM_SENDER_ID = "99999999999999";
private static final String GCM_SERVER_API_KEY = "******************-********************";
private static final boolean ITUNES_PRODUCTION_PUSH = false;
private static final String ITUNES_PRODUCTION_PUSH_CERT = "https://domain.com/linkToP12Prod.p12";
private static final String ITUNES_PRODUCTION_PUSH_CERT_PASSWORD = "ProdPassword";
private static final String ITUNES_DEVELOPMENT_PUSH_CERT = "https://domain.com/linkToP12Dev.p12";
private static final String ITUNES_DEVELOPMENT_PUSH_CERT_PASSWORD = "DevPassword";

PUSH_TOKEN is the easiest, just login to Codename One and select the account tab. It should appear just above the Update Details button. If it isn’t there try logging out and logging back in.

You can get the GCM_SENDER_ID & GCM_SERVER_API_KEY from Google very easily. This assumes you followed our instructions to create a Google project in part 2 of the tutorial. If you skipped that (because you didn’t need a G+ account login) just make sure to create a new project in the Google API console based on the instructions in part 2.

To generate those just go to https://developers.google.com/mobile/add and click “pick a platform”:

chat-app-tutorial-gcm-1

Select “Android App”

chat-app-tutorial-gcm-2

Enter the details for the app and the package

chat-app-tutorial-gcm-3

Click the “Cloud Messaging” option then click the “Enable Google Cloud Messaging” button

chat-app-tutorial-gcm-4

You should now have the values for both GCM_SENDER_ID & GCM_SERVER_API_KEY as illustrated below

chat-app-tutorial-gcm-5

Thanks to our new certificate wizard generating the iOS portion of these flags is now a complete breeze!

We just go thru the certificate wizard and check the flag to enable push:

Enable push the Wizard

Once you finish that wizard and check the enable push flag make sure that the iOS section also has the “Include Push” flag checked. There is a bug in the current plugin where it isn’t enabled automatically.

You should receive instructions to include push by email which should include links that you can just paste into place and passwords. This should be pretty seamless.

Other Code Changes

When you push to a device you need to have the device key which is a unique identifier of the device to which you want to send a push. Unfortunately since we don’t have a server in place we need somehow pass this key from the person we are chatting with. The trick is to embed this key into the Message object and thus update it when we receive a message, this means that we can only send a push message to a person who wrote to us in the past. Not a bad feature all and all but still a limitation…​

To do that we need to do these two simple changes to the Message class:

public Message(JSONObject obj) {
    try {
        time = Long.parseLong(obj.getString("time"));
        senderId = obj.getString("fromId");
        recepientId = obj.getString("toId");
        message = obj.getString("message");
        name = obj.getString("name");
        picture = obj.getString("pic");

        // update the push id for the given user
        if(obj.has("pushId")) {
            String pushId = obj.getString("pushId");
            if(pushId != null) {
                Preferences.set("pid-" + senderId, pushId);
            }
        }
    } catch (JSONException ex) {
        // will this ever happen?
        Log.e(ex);
    }
}

public JSONObject toJSON() {
    String pushId = Push.getPushKey();
    if(pushId != null) {
        JSONObject obj = createJSONObject("fromId", senderId,
                "toId", recepientId,
                "name", name,
                "pic", picture,
                "time", Long.toString(System.currentTimeMillis()),
                "message", message, "pushId", pushId);
        return obj;
    }
    JSONObject obj = createJSONObject("fromId", senderId,
            "toId", recepientId,
            "name", name,
            "pic", picture,
            "time", Long.toString(System.currentTimeMillis()),
            "message", message);
    return obj;
}

This effectively adds a push ID to every message we send if its available and updates a contacts push ID for usage later.

Now we need to register for push, in the end of the start() method in SocialChat.java we add:

// let the login form show before we register the push so the permission screen doesn't appear on a white
// background
Display.getInstance().callSerially(() -> {
    // registering for push after the UI appears
    Hashtable args = new Hashtable();
    args.put(com.codename1.push.Push.GOOGLE_PUSH_KEY, GCM_SENDER_ID);
    Display.getInstance().registerPush(args, true);
});

We do it this way to let the UI appear first.

Previously in the showChatForm method we just sent a message thru PubNub, now we want there to be a fallback that will send the message via push. To do that we need to know that the message wasn’t received by the other side. To discover that we now add a back message in PubNub called “ACK” which will acknowledge the receipt of a message, if an ACK isn’t received that means the message should be sent thru native push…​ To do that we add the class field:

/**
 * Includes messages that received ACK notices from the receiver
 */
private ArrayList<String> pendingAck = new ArrayList<>();

We remove ACK’s automatically in the listenToMessages method as such:

private void listenToMessages() {
    try {
        pb = new Pubnub("pub------------------------------", "sub-------------------------------");
        pb.subscribe(tokenPrefix + uniqueId, new Callback() {
            @Override
            public void successCallback(String channel, Object message, String timetoken) {
                if(message instanceof String) {
                    pendingAck.remove(channel);
                    return;
                }
                Message m = new Message((JSONObject)message);
                pb.publish(tokenPrefix + m.getSenderId(),  "ACK", new Callback() {});
                Display.getInstance().callSerially(() -> {
                    addMessage(m);
                    respond(m);
                });
            }
        });
    } catch(PubnubException err) {
        Log.e(err);
        Dialog.show("Error", "There was a communication error: " + err, "OK", null);
    }
}

In the showChatForm method we need to fallback to push, this is a bit of a large method so I’m only posting the relevant section here:

final Message messageObject = new Message(tokenPrefix + uniqueId, tokenPrefix + d.uniqueId, imageURL, fullName, text);
JSONObject obj = messageObject.toJSON();

String pid = Preferences.get("pid-" + tokenPrefix + d.uniqueId, null);
if(pid != null) {
    // if we have a push address for the contact we can send them a push if they aren't reachable...
    UITimer timeout = new UITimer(() -> {
        if(pendingAck.contains(tokenPrefix + d.uniqueId)) {
            pendingAck.remove(tokenPrefix + d.uniqueId);
            // send two messages, one hidden with the data as JSON for parsing on the client
            // the other one visible with the text that should appear to the user who isn't running
            // the app, this will allow him to launch the app and then receive the hidden message immediately
            // within the app
            String cert = ITUNES_DEVELOPMENT_PUSH_CERT;
            String pass = ITUNES_DEVELOPMENT_PUSH_CERT_PASSWORD;
            if(ITUNES_PRODUCTION_PUSH) {
                cert = ITUNES_PRODUCTION_PUSH_CERT;
                pass = ITUNES_PRODUCTION_PUSH_CERT_PASSWORD;
            }
            if(Push.sendPushMessage(PUSH_TOKEN, text + ";" + obj.toString(),
                    ITUNES_PRODUCTION_PUSH, GCM_SERVER_API_KEY, cert, pass, 3, pid)) {
                t.getUnselectedStyle().setOpacity(255);
                t.repaint();
                addMessage(messageObject);
            } else {
                chatArea.removeComponent(t);
                chatArea.revalidate();
                Dialog.show("Error", "We couldn't reach " + d.name + " thru push", "OK", null);
            }
        }
    });

    timeout.schedule(10000, false, write.getComponentForm());
    if(!pendingAck.contains(tokenPrefix + d.uniqueId)) {
        pendingAck.add(tokenPrefix + d.uniqueId);
    }
}

The way this works is rather simple:

  1. If we have a push id then we create a 10 second timer
  2. When the timer elapses we check if the pendingAck is still pending, if so we need to fall back to push
  3. We have the device push key from the Message class above so sending the push message is pretty easy relatively
  4. We send a type 3 push which includes both a visible and invisible payload separated by a colon (;). The visible payload is just the text of the message whereas the invisible payload is the JSON string we want to add to the message database

And that’s pretty much it for the chat app!

Final Word

When I started off with this tutorial I wasn’t yet familiar with the Parse integration for Codename One. After playing with it quite a bit and being blown away by it I would have architected this whole app on top of it and simplified quite a bit in the process. It would also allow me to track push ID’s keep messages in place and remove some of the issues with going back and forth between PubNub/native push.

I’d still use PubNub without a doubt! Its amazing and very convenient for fast push networking. I think that combining it with Parse would have made this a much better app.

Login via Google/Facebook etc. was probably the most painful part of the app and I’m including push notification within the set of pains. While it is much simpler than it used to be and is simpler than the native/web versions I think the main problem is in the networks opacity and desire to keep the developers close. The pain is less on our side and more on the tedium of creating apps and passing values to Facebook/Google. The APK hash key is just painful, there were things such as “invite a friend” which I just avoided because of the tedium.

I might do a rewrite with those thoughts in mind but I’m more inclined to redo this as a cn1lib rather than an app. The main motivation being end user support for apps, so developers can communicate with users over issues by integrating a single cn1lib into our app. I’m not sure I’ll have time to dig into something like that but I think it should be relatively easy since most of the big pieces (push, cloud storage etc.) are already handled by these great 3rd party services.

Other Posts In This Series

This is an ongoing series of posts including the following parts:

Shai Almog

Shai is the co-founder of Codename One, he has been programming professionally for over 20 years and developing in Java since 96. Shai worked for countless industry leaders including Sun Microsystems where he was a part of the original WTK (Wireless Toolkit) team & the co-creator of LWUIT. He worked with most major device operators/manufactures including Nokia, Samsung, Sony Ericson, Sprint, Vodafone, Verizon, NTT DoCoMo etc. Shai is a blogger and writer who often speaks at conventions. He is a Java One rockstar and top rated speaker for JavaZone, corporate conventions from Oracle, IBM and many others.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button